Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,11 @@
"description": "Flag to enable CSRF protections for UI",
"type": "boolean"
},
"sidebandProgress": {
"description": "Stream per-step validation progress messages to the git client's terminal during a push, using git sideband channel 2 (displayed as \"remote: ...\" lines). Requires the client to negotiate the side-band-64k capability, which all modern git clients do. When disabled, or when the client did not negotiate sideband, pushes receive a single buffered response as before. Operators behind intermediaries that buffer HTTP responses may wish to disable this.",
"type": "boolean",
"default": true
},
"plugins": {
"type": "array",
"description": "List of plugins to integrate on GitProxy's push or pull actions. Each value is either a file path or a module name.",
Expand Down
2 changes: 1 addition & 1 deletion packages/git-proxy-cli/test/testCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { describe, it, beforeAll, afterAll } from 'vitest';

import { setConfigFile } from '../../../src/config/file';
import { invalidateCache } from '../../../src/config';
import { SAMPLE_REPO } from '../../../src/proxy/processors/constants';
import { SAMPLE_REPO } from '../../../src/proxy/constants';
import { handleErrorAndLog } from '../../../src/utils/errors';

setConfigFile(
Expand Down
1 change: 1 addition & 0 deletions proxy.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
"urlShortener": "",
"contactEmail": "",
"csrfProtection": true,
"sidebandProgress": true,
"plugins": [],
"apiAuthentication": [
{
Expand Down
10 changes: 10 additions & 0 deletions src/config/generated/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ export interface GitProxyConfig {
*/
serverPort?: number;
sessionMaxAgeHours?: number;
/**
* Stream per-step validation progress messages to the git client's terminal during a push,
* using git sideband channel 2 (displayed as "remote: ..." lines). Requires the client to
* negotiate the side-band-64k capability, which all modern git clients do. When disabled,
* or when the client did not negotiate sideband, pushes receive a single buffered response
* as before. Operators behind intermediaries that buffer HTTP responses may wish to disable
* this.
*/
sidebandProgress?: boolean;
/**
* List of database sources. The first source in the configuration with enabled=true will be
* used.
Expand Down Expand Up @@ -901,6 +910,7 @@ const typeMap: any = {
{ json: 'rateLimit', js: 'rateLimit', typ: u(undefined, r('RateLimit')) },
{ json: 'serverPort', js: 'serverPort', typ: u(undefined, 3.14) },
{ json: 'sessionMaxAgeHours', js: 'sessionMaxAgeHours', typ: u(undefined, 3.14) },
{ json: 'sidebandProgress', js: 'sidebandProgress', typ: u(undefined, true) },
{ json: 'sink', js: 'sink', typ: u(undefined, a(r('Database'))) },
{ json: 'ssh', js: 'ssh', typ: u(undefined, r('SSH')) },
{ json: 'sslCertPemPath', js: 'sslCertPemPath', typ: u(undefined, '') },
Expand Down
11 changes: 11 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const REQUIRED_TOP_LEVEL_CONFIG_KEYS = [
'rateLimit',
'serverPort',
'sessionMaxAgeHours',
'sidebandProgress',
'sink',
'tempPassword',
'tls',
Expand Down Expand Up @@ -484,6 +485,16 @@ export const getMaxPackSizeBytes = (): number => {
return fallback;
};

/**
* Whether per-step validation progress should be streamed to the git client's
* terminal during a push using git sideband channel 2.
* @return {boolean} true when sideband progress streaming is enabled
*/
export const getSidebandProgressEnabled = (): boolean => {
const config = loadFullConfiguration();
return config.sidebandProgress ?? true;
};

export const getSSHConfig = () => {
const defaultHostKey = {
privateKeyPath: '.ssh/proxy_host_key',
Expand Down
1 change: 1 addition & 0 deletions src/proxy/actions/Action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ class Action {
tagData?: TagData[];
newIdxFiles?: string[];
protocol?: 'https' | 'ssh';
capabilities?: string[];
pullAuthStrategy?:
| 'basic'
| 'ssh-user-key'
Expand Down
41 changes: 40 additions & 1 deletion src/proxy/chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as proc from './processors';
import { Processor } from './processors/types';
import { attemptAutoApproval, attemptAutoRejection } from './actions/autoActions';
import { handleErrorAndLog } from '../utils/errors';
import { createProgressWriter } from './sideband';

const branchPushChain: Processor['exec'][] = [
proc.push.checkEmptyBranch,
Expand Down Expand Up @@ -57,7 +58,40 @@ const defaultActionChain: Processor['exec'][] = [proc.push.checkRepoInAuthorised

let pluginsInserted = false;

export const executeChain = async (req: Request, _res: Response): Promise<Action> => {
const stepProgressLabels: Record<string, string> = {
'checkEmptyBranch.exec': 'Checking for empty branch',
'checkRepoInAuthorisedList.exec': 'Checking repository is authorised',
'checkMessages.exec': 'Checking commit messages',
'checkAuthorEmails.exec': 'Checking author emails',
'checkUserPushPermission.exec': 'Checking push permissions',
'pullRemote.exec': 'Fetching remote repository',
'writePack.exec': 'writing pack data',
'checkHiddenCommits.exec': 'Checking for hidden commits',
'checkIfWaitingAuth.exec': 'Checking approval status',
'executeExternalPreReceiveHook.exec': 'Running pre-receive hook',
'getDiff.exec': 'Computing diff',
'gitleaks.exec': 'Scanning for secrets',
'scanDiff.exec': 'Scanning diff contents',
'blockForAuth.exec': 'Requesting approval',
};

/**
* Obtain the message to display before a chain step.
* @param {Processor['exec']} fn The chain step about to be executed.
* @return {string} The message to display.
*/
const getProgressMessage = (fn: Processor['exec']): string => {
const displayName = (fn as { displayName?: string }).displayName;
if (displayName && stepProgressLabels[displayName]) {
return stepProgressLabels[displayName];
}
if (displayName) {
return `running ${displayName.replace(/\.exec$/, '')}`;
}
return 'running plugin';
};

export const executeChain = async (req: Request, res: Response): Promise<Action> => {
let action: Action = {} as Action;
let checkoutCleanUpRequired = false;

Expand All @@ -71,8 +105,13 @@ export const executeChain = async (req: Request, _res: Response): Promise<Action
// 3) Select the correct chain now that action.actionType is set
const actionFns = await getChain(action);

const progress = createProgressWriter(res, action);

// 4) Execute each step in the selected chain
for (const fn of actionFns) {
if (progress.active && action.continue()) {
progress.message(`${getProgressMessage(fn)}...`);
}
action = await fn(req, action);
if (!action.continue() || action.allowPush) {
break;
Expand Down
15 changes: 14 additions & 1 deletion src/proxy/processors/constants.ts → src/proxy/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { CommitData } from './types';
import { CommitData } from './processors/types';

export const REFS_PREFIX = 'refs/';
export const BRANCH_PREFIX = 'refs/heads/';
Expand Down Expand Up @@ -46,3 +46,16 @@ export const SAMPLE_REPO = {
canAuthorise: ['bob'],
},
};

/**
* Maximum payload bytes for a single side-band-64k packet
* 65520 bytes total minus 4 bytes of pkt-line length and 1 band byte.
*/
export const MAX_SIDEBAND_PAYLOAD_BYTES = 65515;

/** Capability that must be negotiated by the client for streamed progress */
export const SIDE_BAND_64K_CAPABILITY = 'side-band-64k';

/** Capabilities with which the client requests a receive-pack status report.
* The `ng` (rejected ref) line syntax is identical in both protocol versions. */
export const REPORT_STATUS_CAPABILITIES = ['report-status', 'report-status-v2'];
2 changes: 1 addition & 1 deletion src/proxy/processors/pktLineParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* limitations under the License.
*/

import { PACKET_SIZE } from './constants';
import { PACKET_SIZE } from '../constants';

/**
* Parses the packet lines from a buffer into an array of strings.
Expand Down
9 changes: 8 additions & 1 deletion src/proxy/processors/pre-processor/parsePush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import {
PACKET_SIZE,
GIT_OBJECT_TYPE_COMMIT,
GIT_OBJECT_TYPE_TAG,
} from '../constants';
} from '../../constants';
import { parsePacketLines } from '../pktLineParser';
import { getErrorMessage } from '../../../utils/errors';

Expand Down Expand Up @@ -72,6 +72,13 @@ async function exec(req: Request, action: Action): Promise<Action> {
throw new Error('Your push has been blocked. No ref updates found.');
}

// The client's capability list (side-band-64k, report-status, agent=..., etc.)
// is appended after a NUL byte on the first ref update line.
const capabilityString = refUpdates[0].split('\0')[1];
action.capabilities = capabilityString
? capabilityString.trim().split(/\s+/).filter(Boolean)
: [];

const parsedRefs = refUpdates.map((line) => {
const [commitParts] = line.split('\0');
const parts = commitParts.split(' ');
Expand Down
2 changes: 1 addition & 1 deletion src/proxy/processors/push-action/checkEmptyBranch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Request } from 'express';
import simpleGit from 'simple-git';

import { Action, Step } from '../../actions';
import { EMPTY_COMMIT_HASH } from '../constants';
import { EMPTY_COMMIT_HASH } from '../../constants';
import { handleErrorAndLog } from '../../../utils/errors';

const isEmptyBranch = async (action: Action): Promise<boolean> => {
Expand Down
2 changes: 1 addition & 1 deletion src/proxy/processors/push-action/checkHiddenCommits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import path from 'path';
import { Action, Step } from '../../actions';
import { spawnSync } from 'child_process';
import { EMPTY_COMMIT_HASH } from '../constants';
import { EMPTY_COMMIT_HASH } from '../../constants';
import { Request } from 'express';
import { getErrorMessage } from '../../../utils/errors';

Expand Down
2 changes: 1 addition & 1 deletion src/proxy/processors/push-action/getDiff.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Request } from 'express';
import simpleGit from 'simple-git';

import { Action, Step } from '../../actions';
import { EMPTY_COMMIT_HASH } from '../constants';
import { EMPTY_COMMIT_HASH } from '../../constants';
import { getErrorMessage } from '../../../utils/errors';

const exec = async (_req: Request, action: Action): Promise<Action> => {
Expand Down
Loading
Loading