Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
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
11 changes: 10 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,12 @@ 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';
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