Skip to content

Commit 2f000a2

Browse files
add support for no_proxy in RequestService
1 parent be59ec5 commit 2f000a2

4 files changed

Lines changed: 157 additions & 2 deletions

File tree

src/vs/platform/request/common/request.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,12 @@ function registerProxyConfigurations(scope: ConfigurationScope): void {
150150
description: localize('strictSSL', "Controls whether the proxy server certificate should be verified against the list of supplied CAs."),
151151
restricted: true
152152
},
153+
'http.noProxy': {
154+
type: 'array',
155+
default: [],
156+
description: localize('noProxy', "List of hosts which should not use a proxy and are queried directly."),
157+
restricted: true
158+
},
153159
'http.proxyKerberosServicePrincipal': {
154160
type: 'string',
155161
markdownDescription: localize('proxyKerberosServicePrincipal', "Overrides the principal service name for Kerberos authentication with the HTTP proxy. A default based on the proxy hostname is used when this is not set."),

src/vs/platform/request/node/proxy.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { BlockList, IPVersion, isIP } from 'net';
67
import { parse as parseUrl, Url } from 'url';
78
import { isBoolean } from 'vs/base/common/types';
89

@@ -18,9 +19,86 @@ function getSystemProxyURI(requestURL: Url, env: typeof process.env): string | n
1819
return null;
1920
}
2021

22+
export function urlMatchDenyList(requestUrl: Url, denyList: string[]): boolean {
23+
const getIPVersion = (input: string): IPVersion | null => {
24+
const version = isIP(input);
25+
if (![4, 6].includes(version)) {
26+
return null;
27+
}
28+
return version === 4 ? 'ipv4' : 'ipv6';
29+
};
30+
const blockList = new BlockList();
31+
let ipVersion: IPVersion | null = null;
32+
for (let denyHost of denyList) {
33+
if (denyHost === '') {
34+
continue;
35+
}
36+
// Blanket disable
37+
if (denyHost === '*') {
38+
return true;
39+
}
40+
// Full match
41+
if (requestUrl.hostname === denyHost || requestUrl.host === denyHost) {
42+
return true;
43+
}
44+
// Remove leading dots to validate suffixes
45+
if (denyHost[0] === '.') {
46+
denyHost = denyHost.substring(1);
47+
}
48+
if (requestUrl.hostname?.endsWith(denyHost)) {
49+
return true;
50+
}
51+
// IP+CIDR notation support, add those to our intermediate
52+
// blocklist to be checked afterwards
53+
if (ipVersion = getIPVersion(denyHost)) {
54+
blockList.addAddress(denyHost, ipVersion);
55+
}
56+
const cidrPrefixMatch = denyHost.match(/^(?<ip>.*)\/(?<cidrPrefix>\d+)$/);
57+
if (cidrPrefixMatch && cidrPrefixMatch.groups) {
58+
const matchedIP = cidrPrefixMatch.groups['ip'];
59+
const matchedPrefix = cidrPrefixMatch.groups['cidrPrefix'];
60+
if (matchedIP && matchedPrefix) {
61+
ipVersion = getIPVersion(matchedIP);
62+
const prefix = Number(matchedPrefix);
63+
if (ipVersion && prefix) {
64+
blockList.addSubnet(matchedIP, prefix, ipVersion);
65+
}
66+
}
67+
}
68+
}
69+
70+
// Do a final check using block list if the requestUrl is an IP.
71+
// Importantly domain names are not first resolved to an IP to do this check in
72+
// line with how the rest of the ecosystem behaves
73+
const hostname = requestUrl.hostname;
74+
if (hostname && (ipVersion = getIPVersion(hostname)) && blockList.check(hostname, ipVersion)) {
75+
return true;
76+
}
77+
78+
return false;
79+
}
80+
81+
export function shouldProxyUrl(requestUrl: Url, noProxyConfig: string[], env: typeof process.env): boolean {
82+
// If option is set use that over anything else
83+
if (noProxyConfig.length !== 0) {
84+
return !urlMatchDenyList(requestUrl, noProxyConfig);
85+
}
86+
87+
// Else look at the environment
88+
const noProxyEnv = env.no_proxy || env.NO_PROXY || null;
89+
if (noProxyEnv) {
90+
// Values are expected to be comma-separated, leading/trailing whitespaces are also removed from entries
91+
const envDenyList = noProxyEnv.split(',').map(entry => entry.trim());
92+
return !urlMatchDenyList(requestUrl, envDenyList);
93+
}
94+
95+
return true;
96+
}
97+
2198
export interface IOptions {
2299
proxyUrl?: string;
23100
strictSSL?: boolean;
101+
noProxy?: string[];
24102
}
25103

26104
export async function getProxyAgent(rawRequestURL: string, env: typeof process.env, options: IOptions = {}): Promise<Agent> {
@@ -31,6 +109,10 @@ export async function getProxyAgent(rawRequestURL: string, env: typeof process.e
31109
return null;
32110
}
33111

112+
if (!shouldProxyUrl(requestURL, options.noProxy || [], env)) {
113+
return null;
114+
}
115+
34116
const proxyEndpoint = parseUrl(proxyURL);
35117

36118
if (!/^https?:$/.test(proxyEndpoint.protocol || '')) {

src/vs/platform/request/node/requestService.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ interface IHTTPConfiguration {
2525
proxy?: string;
2626
proxyStrictSSL?: boolean;
2727
proxyAuthorization?: string;
28+
noProxy?: string[];
2829
}
2930

3031
export interface IRawRequestFunction {
@@ -48,6 +49,7 @@ export class RequestService extends AbstractRequestService implements IRequestSe
4849

4950
private proxyUrl?: string;
5051
private strictSSL: boolean | undefined;
52+
private noProxy: string[] = [];
5153
private authorization?: string;
5254
private shellEnvErrorLogged?: boolean;
5355

@@ -72,10 +74,11 @@ export class RequestService extends AbstractRequestService implements IRequestSe
7274
this.proxyUrl = config?.proxy;
7375
this.strictSSL = !!config?.proxyStrictSSL;
7476
this.authorization = config?.proxyAuthorization;
77+
this.noProxy = config?.noProxy || [];
7578
}
7679

7780
async request(options: NodeRequestOptions, token: CancellationToken): Promise<IRequestContext> {
78-
const { proxyUrl, strictSSL } = this;
81+
const { proxyUrl, strictSSL, noProxy } = this;
7982

8083
let shellEnv: typeof process.env | undefined = undefined;
8184
try {
@@ -91,7 +94,7 @@ export class RequestService extends AbstractRequestService implements IRequestSe
9194
...process.env,
9295
...shellEnv
9396
};
94-
const agent = options.agent ? options.agent : await getProxyAgent(options.url || '', env, { proxyUrl, strictSSL });
97+
const agent = options.agent ? options.agent : await getProxyAgent(options.url || '', env, { proxyUrl, strictSSL, noProxy });
9598

9699
options.agent = agent;
97100
options.strictSSL = strictSSL;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as assert from 'assert';
7+
import { parse as parseUrl, Url } from 'url';
8+
import { shouldProxyUrl, urlMatchDenyList } from 'vs/platform/request/node/proxy';
9+
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
10+
11+
12+
suite("proxy support", () => {
13+
const urlWithDomain = parseUrl("https://example.com/some/path");
14+
const urlWithSubdomain = parseUrl("https://internal.example.com/some/path");
15+
const urlWithIPv4 = parseUrl("https://127.0.0.1/some/path");
16+
const urlWithIPv6 = parseUrl("https://[::1]/some/path");
17+
18+
test("returns true if no denylists are provided", () => {
19+
assert.strictEqual(shouldProxyUrl(urlWithDomain, [], {}), true);
20+
});
21+
22+
test("gives precedence to direct config value rather than environment", () => {
23+
assert.strictEqual(shouldProxyUrl(urlWithDomain, ["otherexample.com"], { no_proxy: "example.com" }), true);
24+
assert.strictEqual(shouldProxyUrl(urlWithDomain, ["example.com"], { no_proxy: "otherexample.com" }), false);
25+
});
26+
27+
test("match wildcard", () => {
28+
assert.strictEqual(urlMatchDenyList(urlWithDomain, ['*']), true);
29+
assert.strictEqual(urlMatchDenyList(urlWithSubdomain, ['*']), true);
30+
assert.strictEqual(urlMatchDenyList(urlWithIPv4, ['*']), true);
31+
assert.strictEqual(urlMatchDenyList(urlWithIPv6, ['*']), true);
32+
});
33+
34+
test("match direct hostname", () => {
35+
assert.strictEqual(urlMatchDenyList(urlWithDomain, ['example.com']), true);
36+
assert.strictEqual(urlMatchDenyList(urlWithDomain, ['otherexample.com']), false);
37+
// Technically the following are a suffix match but it's a known behavior in the ecosystem
38+
assert.strictEqual(urlMatchDenyList(urlWithDomain, ['.example.com']), true);
39+
assert.strictEqual(urlMatchDenyList(urlWithDomain, ['.otherexample.com']), false);
40+
});
41+
42+
test("match hostname suffixes", () => {
43+
assert.strictEqual(urlMatchDenyList(urlWithSubdomain, ['example.com']), true);
44+
assert.strictEqual(urlMatchDenyList(urlWithSubdomain, ['.example.com']), true);
45+
assert.strictEqual(urlMatchDenyList(urlWithSubdomain, ['otherexample.com']), false);
46+
assert.strictEqual(urlMatchDenyList(urlWithSubdomain, ['.otherexample.com']), false);
47+
});
48+
49+
test("match IP addresses", () => {
50+
assert.strictEqual(urlMatchDenyList(urlWithIPv4, ['example.com']), false);
51+
assert.strictEqual(urlMatchDenyList(urlWithIPv6, ['example.com']), false);
52+
assert.strictEqual(urlMatchDenyList(urlWithIPv4, ['127.0.0.1']), true);
53+
assert.strictEqual(urlMatchDenyList(urlWithIPv6, ['::1']), true);
54+
});
55+
56+
test("match IP addresses with range deny list", () => {
57+
assert.strictEqual(urlMatchDenyList(urlWithIPv4, ['127.0.0.0/8']), true);
58+
assert.strictEqual(urlMatchDenyList(urlWithIPv4, ['10.0.0.0/8']), false);
59+
assert.strictEqual(urlMatchDenyList(urlWithIPv6, ['::0/64']), true);
60+
assert.strictEqual(urlMatchDenyList(urlWithIPv6, ['100::0/64']), false);
61+
});
62+
63+
ensureNoDisposablesAreLeakedInTestSuite();
64+
});

0 commit comments

Comments
 (0)