Skip to content

Commit be51974

Browse files
committed
feat: add upstream proxy configuration for outbound requests
1 parent 9f148a4 commit be51974

8 files changed

Lines changed: 310 additions & 1 deletion

File tree

config.schema.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,29 @@
367367
}
368368
}
369369
}
370+
},
371+
"upstreamProxy": {
372+
"description": "Configuration for routing outbound requests to upstream Git hosts via an HTTP(S) proxy.",
373+
"type": "object",
374+
"properties": {
375+
"enabled": {
376+
"type": "boolean",
377+
"description": "Whether to use an outbound HTTP(S) proxy for upstream Git hosts."
378+
},
379+
"url": {
380+
"type": "string",
381+
"description": "Proxy URL used for outbound connections to upstream Git hosts when set.",
382+
"format": "uri"
383+
},
384+
"noProxy": {
385+
"type": "array",
386+
"description": "Additional hostnames or domain suffixes that should bypass the upstream proxy.",
387+
"items": {
388+
"type": "string"
389+
}
390+
}
391+
},
392+
"additionalProperties": false
370393
}
371394
},
372395
"definitions": {

docs/Architecture.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,26 @@ Currently supports the following out-of-the-box:
222222
- ActiveDirectory auth configuration for querying via a REST API rather than LDAP
223223
- Gitleaks configuration
224224

225+
#### `upstreamProxy`
226+
227+
Configures routing of outbound requests from the GitProxy server to upstream Git hosts (e.g. GitHub, GitLab) via an HTTP(S) proxy. Use this when the server runs in an environment where direct Internet access is not allowed and all traffic must go through a corporate web proxy ("proxying the proxy").
228+
229+
- **`enabled`** (boolean): When `true`, outbound connections to upstream Git hosts use the configured proxy. When `false`, the proxy is not used even if `url` or environment variables are set.
230+
- **`url`** (string): The HTTP(S) proxy URL (e.g. `http://proxy.corp.local:8080` or `http://user:pass@proxy.corp.local:8080`). If omitted, GitProxy falls back to the `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY` or `http_proxy` environment variables (first defined wins).
231+
- **`noProxy`** (array of strings, optional): Hostnames or domain suffixes for which the proxy should be bypassed (e.g. internal Git hosts). Combined with the `NO_PROXY` / `no_proxy` environment variable.
232+
233+
Example:
234+
235+
```json
236+
"upstreamProxy": {
237+
"enabled": true,
238+
"url": "http://proxy.corp.local:8080",
239+
"noProxy": ["github.corp.local", "gitlab.corp.local"]
240+
}
241+
```
242+
243+
If `upstreamProxy` is not configured, setting only `HTTPS_PROXY` (or `HTTP_PROXY`) in the environment will also enable use of that proxy for outbound connections, unless `enabled` is explicitly set to `false` in config.
244+
225245
#### `commitConfig`
226246

227247
Used in [`checkCommitMessages`](./Processors.md#checkcommitmessages), [`checkAuthorEmails`](./Processors.md#checkauthoremails) and [`scanDiff`](./Processors.md#scandiff) processors to block pushes depending on the given rules.

package-lock.json

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@
107107
"express-session": "^1.19.0",
108108
"font-awesome": "^4.7.0",
109109
"history": "5.3.0",
110+
"https-proxy-agent": "^7.0.6",
110111
"isomorphic-git": "^1.36.3",
111112
"jsonwebtoken": "^9.0.3",
112113
"load-plugin": "^6.0.3",

src/config/generated/config.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@ export interface GitProxyConfig {
101101
* UI routes that require authentication (logged in or admin)
102102
*/
103103
uiRouteAuth?: UIRouteAuth;
104+
/**
105+
* Configuration for routing outbound requests to upstream Git hosts via an HTTP(S) proxy.
106+
*/
107+
upstreamProxy?: UpstreamProxy;
104108
/**
105109
* Customisable URL shortener to share in proxy responses and warnings
106110
*/
@@ -563,6 +567,24 @@ export interface RouteAuthRule {
563567
[property: string]: any;
564568
}
565569

570+
/**
571+
* Configuration for routing outbound requests to upstream Git hosts via an HTTP(S) proxy.
572+
*/
573+
export interface UpstreamProxy {
574+
/**
575+
* Whether to use an outbound HTTP(S) proxy for upstream Git hosts.
576+
*/
577+
enabled?: boolean;
578+
/**
579+
* Additional hostnames or domain suffixes that should bypass the upstream proxy.
580+
*/
581+
noProxy?: string[];
582+
/**
583+
* Proxy URL used for outbound connections to upstream Git hosts when set.
584+
*/
585+
url?: string;
586+
}
587+
566588
// Converts JSON strings to/from your types
567589
// and asserts the results of JSON.parse at runtime
568590
export class Convert {
@@ -780,6 +802,7 @@ const typeMap: any = {
780802
{ json: 'tempPassword', js: 'tempPassword', typ: u(undefined, r('TempPassword')) },
781803
{ json: 'tls', js: 'tls', typ: u(undefined, r('TLS')) },
782804
{ json: 'uiRouteAuth', js: 'uiRouteAuth', typ: u(undefined, r('UIRouteAuth')) },
805+
{ json: 'upstreamProxy', js: 'upstreamProxy', typ: u(undefined, r('UpstreamProxy')) },
783806
{ json: 'urlShortener', js: 'urlShortener', typ: u(undefined, '') },
784807
],
785808
false,
@@ -981,6 +1004,14 @@ const typeMap: any = {
9811004
],
9821005
'any',
9831006
),
1007+
UpstreamProxy: o(
1008+
[
1009+
{ json: 'enabled', js: 'enabled', typ: u(undefined, true) },
1010+
{ json: 'noProxy', js: 'noProxy', typ: u(undefined, a('')) },
1011+
{ json: 'url', js: 'url', typ: u(undefined, '') },
1012+
],
1013+
false,
1014+
),
9841015
AuthenticationElementType: ['ActiveDirectory', 'jwt', 'local', 'openidconnect'],
9851016
DatabaseType: ['fs', 'mongo'],
9861017
};

src/config/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@ export const getProxyUrl = (): string | undefined => {
149149
return config.proxyUrl;
150150
};
151151

152+
// Get upstream proxy configuration
153+
export const getUpstreamProxyConfig = () => {
154+
const config = loadFullConfiguration();
155+
return config.upstreamProxy || {};
156+
};
157+
152158
// Gets a list of authorised repositories
153159
export const getAuthorisedList = () => {
154160
const config = loadFullConfiguration();

src/proxy/routes/index.ts

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import { processUrlPath, validGitRequest } from './helper';
2323
import { getAllProxiedHosts } from '../../db';
2424
import { ProxyOptions } from 'express-http-proxy';
2525
import { getErrorMessage, handleAndLogError } from '../../utils/errors';
26+
import { getUpstreamProxyConfig } from '../../config';
27+
import { HttpsProxyAgent } from 'https-proxy-agent';
28+
import { OutgoingHttpHeaders, RequestOptions } from 'http';
2629

2730
enum ActionType {
2831
ALLOWED = 'Allowed',
@@ -144,7 +147,100 @@ const getRequestPathResolver: (prefix: string) => ProxyOptions['proxyReqPathReso
144147
};
145148
};
146149

147-
const proxyReqOptDecorator: ProxyOptions['proxyReqOptDecorator'] = (proxyReqOpts) => proxyReqOpts;
150+
const getEnvProxyUrl = () =>
151+
process.env.HTTPS_PROXY ||
152+
process.env.https_proxy ||
153+
process.env.HTTP_PROXY ||
154+
process.env.http_proxy;
155+
156+
const getEnvNoProxyList = (): string[] => {
157+
const noProxy = process.env.NO_PROXY || process.env.no_proxy;
158+
if (!noProxy) {
159+
return [];
160+
}
161+
return noProxy
162+
.split(',')
163+
.map((entry) => entry.trim())
164+
.filter((entry) => entry.length > 0);
165+
};
166+
167+
const hostMatchesNoProxy = (host: string | null | undefined, noProxyList: string[]): boolean => {
168+
if (!host) {
169+
return false;
170+
}
171+
172+
const hostname = host.split(':')[0];
173+
174+
return noProxyList.some((pattern) => {
175+
if (!pattern) {
176+
return false;
177+
}
178+
const trimmed = pattern.trim();
179+
if (trimmed === '') {
180+
return false;
181+
}
182+
183+
// Exact match
184+
if (hostname === trimmed) {
185+
return true;
186+
}
187+
188+
// Domain suffix match, e.g. example.com matches foo.example.com
189+
if (hostname.endsWith(`.${trimmed}`)) {
190+
return true;
191+
}
192+
193+
return false;
194+
});
195+
};
196+
197+
const buildUpstreamProxyAgent = (
198+
proxyReqOpts: Omit<RequestOptions, 'headers'> & {
199+
headers: OutgoingHttpHeaders;
200+
},
201+
) => {
202+
const upstreamProxyConfig = getUpstreamProxyConfig();
203+
204+
const configuredUrl = upstreamProxyConfig.url;
205+
const envUrl = getEnvProxyUrl();
206+
207+
const proxyUrl = configuredUrl || envUrl;
208+
209+
// If nothing is configured, do not use a proxy
210+
if (!proxyUrl) {
211+
return undefined;
212+
}
213+
214+
// If config explicitly disabled the proxy, do not use it
215+
if (upstreamProxyConfig.enabled === false) {
216+
return undefined;
217+
}
218+
219+
const host: string | null | undefined = proxyReqOpts.host || proxyReqOpts.hostname;
220+
221+
const configNoProxy = upstreamProxyConfig.noProxy ? upstreamProxyConfig.noProxy : [];
222+
const envNoProxy = getEnvNoProxyList();
223+
const combinedNoProxy = [...configNoProxy, ...envNoProxy];
224+
225+
if (hostMatchesNoProxy(host, combinedNoProxy)) {
226+
return undefined;
227+
}
228+
229+
return new HttpsProxyAgent(proxyUrl);
230+
};
231+
232+
const proxyReqOptDecorator: ProxyOptions['proxyReqOptDecorator'] = (proxyReqOpts, _srcReq) => {
233+
const agent = buildUpstreamProxyAgent(proxyReqOpts);
234+
235+
if (!agent) {
236+
return proxyReqOpts;
237+
}
238+
239+
return {
240+
...proxyReqOpts,
241+
agent,
242+
};
243+
};
148244

149245
const proxyReqBodyDecorator: ProxyOptions['proxyReqBodyDecorator'] = (bodyContent, srcReq) => {
150246
if (srcReq.method === 'GET') {
@@ -273,4 +369,5 @@ export {
273369
isPackPost,
274370
extractRawBody,
275371
validGitRequest,
372+
buildUpstreamProxyAgent,
276373
};

0 commit comments

Comments
 (0)