Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/node/proxyResolver.ts
3296 views
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 { IExtHostWorkspaceProvider } from '../common/extHostWorkspace.js';
7
import { ConfigurationInspect, ExtHostConfigProvider } from '../common/extHostConfiguration.js';
8
import { MainThreadTelemetryShape } from '../common/extHost.protocol.js';
9
import { IExtensionHostInitData } from '../../services/extensions/common/extensionHostProtocol.js';
10
import { ExtHostExtensionService } from './extHostExtensionService.js';
11
import { URI } from '../../../base/common/uri.js';
12
import { ILogService, LogLevel as LogServiceLevel } from '../../../platform/log/common/log.js';
13
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
14
import { LogLevel, createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting, ProxyAgentParams, createNetPatch, loadSystemCertificates, ResolveProxyWithRequest } from '@vscode/proxy-agent';
15
import { AuthInfo } from '../../../platform/request/common/request.js';
16
import { DisposableStore } from '../../../base/common/lifecycle.js';
17
import { createRequire } from 'node:module';
18
import type * as undiciType from 'undici-types';
19
import type * as tlsType from 'tls';
20
import { lookupKerberosAuthorization } from '../../../platform/request/node/requestService.js';
21
import * as proxyAgent from '@vscode/proxy-agent';
22
23
const require = createRequire(import.meta.url);
24
const http = require('http');
25
const https = require('https');
26
const tls: typeof tlsType = require('tls');
27
const net = require('net');
28
29
const systemCertificatesV2Default = false;
30
const useElectronFetchDefault = false;
31
32
export function connectProxyResolver(
33
extHostWorkspace: IExtHostWorkspaceProvider,
34
configProvider: ExtHostConfigProvider,
35
extensionService: ExtHostExtensionService,
36
extHostLogService: ILogService,
37
mainThreadTelemetry: MainThreadTelemetryShape,
38
initData: IExtensionHostInitData,
39
disposables: DisposableStore,
40
) {
41
42
const isRemote = initData.remote.isRemote;
43
const useHostProxyDefault = initData.environment.useHostProxy ?? !isRemote;
44
const fallbackToLocalKerberos = useHostProxyDefault;
45
const loadLocalCertificates = useHostProxyDefault;
46
const isUseHostProxyEnabled = () => !isRemote || configProvider.getConfiguration('http').get<boolean>('useLocalProxyConfiguration', useHostProxyDefault);
47
const params: ProxyAgentParams = {
48
resolveProxy: url => extHostWorkspace.resolveProxy(url),
49
lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote, fallbackToLocalKerberos),
50
getProxyURL: () => getExtHostConfigValue<string>(configProvider, isRemote, 'http.proxy'),
51
getProxySupport: () => getExtHostConfigValue<ProxySupportSetting>(configProvider, isRemote, 'http.proxySupport') || 'off',
52
getNoProxyConfig: () => getExtHostConfigValue<string[]>(configProvider, isRemote, 'http.noProxy') || [],
53
isAdditionalFetchSupportEnabled: () => getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.fetchAdditionalSupport', true),
54
addCertificatesV1: () => certSettingV1(configProvider, isRemote),
55
addCertificatesV2: () => certSettingV2(configProvider, isRemote),
56
log: extHostLogService,
57
getLogLevel: () => {
58
const level = extHostLogService.getLevel();
59
switch (level) {
60
case LogServiceLevel.Trace: return LogLevel.Trace;
61
case LogServiceLevel.Debug: return LogLevel.Debug;
62
case LogServiceLevel.Info: return LogLevel.Info;
63
case LogServiceLevel.Warning: return LogLevel.Warning;
64
case LogServiceLevel.Error: return LogLevel.Error;
65
case LogServiceLevel.Off: return LogLevel.Off;
66
default: return never(level);
67
}
68
function never(level: never) {
69
extHostLogService.error('Unknown log level', level);
70
return LogLevel.Debug;
71
}
72
},
73
proxyResolveTelemetry: () => { },
74
isUseHostProxyEnabled,
75
loadAdditionalCertificates: async () => {
76
const promises: Promise<string[]>[] = [];
77
if (initData.remote.isRemote) {
78
promises.push(loadSystemCertificates({ log: extHostLogService }));
79
}
80
if (loadLocalCertificates) {
81
extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading certificates from main process');
82
const certs = extHostWorkspace.loadCertificates(); // Loading from main process to share cache.
83
certs.then(certs => extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loaded certificates from main process', certs.length));
84
promises.push(certs);
85
}
86
// Using https.globalAgent because it is shared with proxy.test.ts and mutable.
87
if (initData.environment.extensionTestsLocationURI && (https.globalAgent as any).testCertificates?.length) {
88
extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading test certificates');
89
promises.push(Promise.resolve((https.globalAgent as any).testCertificates as string[]));
90
}
91
return (await Promise.all(promises)).flat();
92
},
93
env: process.env,
94
};
95
const { resolveProxyWithRequest, resolveProxyURL } = createProxyResolver(params);
96
const target = (proxyAgent as any).default || proxyAgent;
97
target.resolveProxyURL = resolveProxyURL;
98
99
patchGlobalFetch(params, configProvider, mainThreadTelemetry, initData, resolveProxyURL, disposables);
100
101
const lookup = createPatchedModules(params, resolveProxyWithRequest);
102
return configureModuleLoading(extensionService, lookup);
103
}
104
105
const unsafeHeaders = [
106
'content-length',
107
'host',
108
'trailer',
109
'te',
110
'upgrade',
111
'cookie2',
112
'keep-alive',
113
'transfer-encoding',
114
'set-cookie',
115
];
116
117
function patchGlobalFetch(params: ProxyAgentParams, configProvider: ExtHostConfigProvider, mainThreadTelemetry: MainThreadTelemetryShape, initData: IExtensionHostInitData, resolveProxyURL: (url: string) => Promise<string | undefined>, disposables: DisposableStore) {
118
if (!(globalThis as any).__vscodeOriginalFetch) {
119
const originalFetch = globalThis.fetch;
120
(globalThis as any).__vscodeOriginalFetch = originalFetch;
121
const patchedFetch = proxyAgent.createFetchPatch(params, originalFetch, resolveProxyURL);
122
(globalThis as any).__vscodePatchedFetch = patchedFetch;
123
let useElectronFetch = false;
124
if (!initData.remote.isRemote) {
125
useElectronFetch = configProvider.getConfiguration('http').get<boolean>('electronFetch', useElectronFetchDefault);
126
disposables.add(configProvider.onDidChangeConfiguration(e => {
127
if (e.affectsConfiguration('http.electronFetch')) {
128
useElectronFetch = configProvider.getConfiguration('http').get<boolean>('electronFetch', useElectronFetchDefault);
129
}
130
}));
131
}
132
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
133
globalThis.fetch = async function fetch(input: string | URL | Request, init?: RequestInit) {
134
function getRequestProperty(name: keyof Request & keyof RequestInit) {
135
return init && name in init ? init[name] : typeof input === 'object' && 'cache' in input ? input[name] : undefined;
136
}
137
// Limitations: https://github.com/electron/electron/pull/36733#issuecomment-1405615494
138
// net.fetch fails on manual redirect: https://github.com/electron/electron/issues/43715
139
const urlString = typeof input === 'string' ? input : 'cache' in input ? input.url : input.toString();
140
const isDataUrl = urlString.startsWith('data:');
141
if (isDataUrl) {
142
recordFetchFeatureUse(mainThreadTelemetry, 'data');
143
}
144
const isBlobUrl = urlString.startsWith('blob:');
145
if (isBlobUrl) {
146
recordFetchFeatureUse(mainThreadTelemetry, 'blob');
147
}
148
const isManualRedirect = getRequestProperty('redirect') === 'manual';
149
if (isManualRedirect) {
150
recordFetchFeatureUse(mainThreadTelemetry, 'manualRedirect');
151
}
152
const integrity = getRequestProperty('integrity');
153
if (integrity) {
154
recordFetchFeatureUse(mainThreadTelemetry, 'integrity');
155
}
156
if (!useElectronFetch || isDataUrl || isBlobUrl || isManualRedirect || integrity) {
157
const response = await patchedFetch(input, init);
158
monitorResponseProperties(mainThreadTelemetry, response, urlString);
159
return response;
160
}
161
// Unsupported headers: https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc;l=32;drc=ee7299f8961a1b05a3554efcc496b6daa0d7f6e1
162
if (init?.headers) {
163
const headers = new Headers(init.headers);
164
for (const header of unsafeHeaders) {
165
headers.delete(header);
166
}
167
init = { ...init, headers };
168
}
169
// Support for URL: https://github.com/electron/electron/issues/43712
170
const electronInput = input instanceof URL ? input.toString() : input;
171
const electron = require('electron');
172
const response = await electron.net.fetch(electronInput, init);
173
monitorResponseProperties(mainThreadTelemetry, response, urlString);
174
return response;
175
};
176
}
177
}
178
179
function monitorResponseProperties(mainThreadTelemetry: MainThreadTelemetryShape, response: Response, urlString: string) {
180
const originalUrl = response.url;
181
Object.defineProperty(response, 'url', {
182
get() {
183
recordFetchFeatureUse(mainThreadTelemetry, 'url');
184
return originalUrl || urlString;
185
}
186
});
187
const originalType = response.type;
188
Object.defineProperty(response, 'type', {
189
get() {
190
recordFetchFeatureUse(mainThreadTelemetry, 'typeProperty');
191
return originalType !== 'default' ? originalType : 'basic';
192
}
193
});
194
}
195
196
type FetchFeatureUseClassification = {
197
owner: 'chrmarti';
198
comment: 'Data about fetch API use';
199
url: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the url property was used.' };
200
typeProperty: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the type property was used.' };
201
data: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a data URL was used.' };
202
blob: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a blob URL was used.' };
203
integrity: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the integrity property was used.' };
204
manualRedirect: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a manual redirect was used.' };
205
};
206
207
type FetchFeatureUseEvent = {
208
url: number;
209
typeProperty: number;
210
data: number;
211
blob: number;
212
integrity: number;
213
manualRedirect: number;
214
};
215
216
const fetchFeatureUse: FetchFeatureUseEvent = {
217
url: 0,
218
typeProperty: 0,
219
data: 0,
220
blob: 0,
221
integrity: 0,
222
manualRedirect: 0,
223
};
224
225
let timer: Timeout | undefined;
226
const enableFeatureUseTelemetry = false;
227
function recordFetchFeatureUse(mainThreadTelemetry: MainThreadTelemetryShape, feature: keyof typeof fetchFeatureUse) {
228
if (enableFeatureUseTelemetry && !fetchFeatureUse[feature]++) {
229
if (timer) {
230
clearTimeout(timer);
231
}
232
timer = setTimeout(() => {
233
mainThreadTelemetry.$publicLog2<FetchFeatureUseEvent, FetchFeatureUseClassification>('fetchFeatureUse', fetchFeatureUse);
234
}, 10000); // collect additional features for 10 seconds
235
(timer as unknown as NodeJS.Timeout).unref?.();
236
}
237
}
238
239
function createPatchedModules(params: ProxyAgentParams, resolveProxy: ResolveProxyWithRequest) {
240
241
function mergeModules(module: any, patch: any) {
242
const target = module.default || module;
243
target.__vscodeOriginal = Object.assign({}, target);
244
return Object.assign(target, patch);
245
}
246
247
return {
248
http: mergeModules(http, createHttpPatch(params, http, resolveProxy)),
249
https: mergeModules(https, createHttpPatch(params, https, resolveProxy)),
250
net: mergeModules(net, createNetPatch(params, net)),
251
tls: mergeModules(tls, createTlsPatch(params, tls))
252
};
253
}
254
255
function certSettingV1(configProvider: ExtHostConfigProvider, isRemote: boolean) {
256
return !getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.experimental.systemCertificatesV2', systemCertificatesV2Default) && !!getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.systemCertificates');
257
}
258
259
function certSettingV2(configProvider: ExtHostConfigProvider, isRemote: boolean) {
260
return !!getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.experimental.systemCertificatesV2', systemCertificatesV2Default) && !!getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.systemCertificates');
261
}
262
263
const modulesCache = new Map<IExtensionDescription | undefined, { http?: typeof http; https?: typeof https; undici?: typeof undiciType }>();
264
function configureModuleLoading(extensionService: ExtHostExtensionService, lookup: ReturnType<typeof createPatchedModules>): Promise<void> {
265
return extensionService.getExtensionPathIndex()
266
.then(extensionPaths => {
267
const node_module = require('module');
268
const original = node_module._load;
269
node_module._load = function load(request: string, parent: { filename: string }, isMain: boolean) {
270
if (request === 'net') {
271
return lookup.net;
272
}
273
274
if (request === 'tls') {
275
return lookup.tls;
276
}
277
278
if (request !== 'http' && request !== 'https' && request !== 'undici') {
279
return original.apply(this, arguments);
280
}
281
282
const ext = extensionPaths.findSubstr(URI.file(parent.filename));
283
let cache = modulesCache.get(ext);
284
if (!cache) {
285
modulesCache.set(ext, cache = {});
286
}
287
if (!cache[request]) {
288
if (request === 'undici') {
289
const undici = original.apply(this, arguments);
290
proxyAgent.patchUndici(undici);
291
cache[request] = undici;
292
} else {
293
const mod = lookup[request];
294
cache[request] = <any>{ ...mod }; // Copy to work around #93167.
295
}
296
}
297
return cache[request];
298
};
299
});
300
}
301
302
async function lookupProxyAuthorization(
303
extHostWorkspace: IExtHostWorkspaceProvider,
304
extHostLogService: ILogService,
305
mainThreadTelemetry: MainThreadTelemetryShape,
306
configProvider: ExtHostConfigProvider,
307
proxyAuthenticateCache: Record<string, string | string[] | undefined>,
308
basicAuthCache: Record<string, string | undefined>,
309
isRemote: boolean,
310
fallbackToLocalKerberos: boolean,
311
proxyURL: string,
312
proxyAuthenticate: string | string[] | undefined,
313
state: { kerberosRequested?: boolean; basicAuthCacheUsed?: boolean; basicAuthAttempt?: number }
314
): Promise<string | undefined> {
315
const cached = proxyAuthenticateCache[proxyURL];
316
if (proxyAuthenticate) {
317
proxyAuthenticateCache[proxyURL] = proxyAuthenticate;
318
}
319
extHostLogService.trace('ProxyResolver#lookupProxyAuthorization callback', `proxyURL:${proxyURL}`, `proxyAuthenticate:${proxyAuthenticate}`, `proxyAuthenticateCache:${cached}`);
320
const header = proxyAuthenticate || cached;
321
const authenticate = Array.isArray(header) ? header : typeof header === 'string' ? [header] : [];
322
sendTelemetry(mainThreadTelemetry, authenticate, isRemote);
323
if (authenticate.some(a => /^(Negotiate|Kerberos)( |$)/i.test(a)) && !state.kerberosRequested) {
324
state.kerberosRequested = true;
325
326
try {
327
const spnConfig = getExtHostConfigValue<string>(configProvider, isRemote, 'http.proxyKerberosServicePrincipal');
328
const response = await lookupKerberosAuthorization(proxyURL, spnConfig, extHostLogService, 'ProxyResolver#lookupProxyAuthorization');
329
return 'Negotiate ' + response;
330
} catch (err) {
331
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err);
332
}
333
334
if (isRemote && fallbackToLocalKerberos) {
335
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication lookup on host', `proxyURL:${proxyURL}`);
336
const auth = await extHostWorkspace.lookupKerberosAuthorization(proxyURL);
337
if (auth) {
338
return 'Negotiate ' + auth;
339
}
340
}
341
}
342
const basicAuthHeader = authenticate.find(a => /^Basic( |$)/i.test(a));
343
if (basicAuthHeader) {
344
try {
345
const cachedAuth = basicAuthCache[proxyURL];
346
if (cachedAuth) {
347
if (state.basicAuthCacheUsed) {
348
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication deleting cached credentials', `proxyURL:${proxyURL}`);
349
delete basicAuthCache[proxyURL];
350
} else {
351
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication using cached credentials', `proxyURL:${proxyURL}`);
352
state.basicAuthCacheUsed = true;
353
return cachedAuth;
354
}
355
}
356
state.basicAuthAttempt = (state.basicAuthAttempt || 0) + 1;
357
const realm = / realm="([^"]+)"/i.exec(basicAuthHeader)?.[1];
358
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication lookup', `proxyURL:${proxyURL}`, `realm:${realm}`);
359
const url = new URL(proxyURL);
360
const authInfo: AuthInfo = {
361
scheme: 'basic',
362
host: url.hostname,
363
port: Number(url.port),
364
realm: realm || '',
365
isProxy: true,
366
attempt: state.basicAuthAttempt,
367
};
368
const credentials = await extHostWorkspace.lookupAuthorization(authInfo);
369
if (credentials) {
370
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication received credentials', `proxyURL:${proxyURL}`, `realm:${realm}`);
371
const auth = 'Basic ' + Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
372
basicAuthCache[proxyURL] = auth;
373
return auth;
374
} else {
375
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication received no credentials', `proxyURL:${proxyURL}`, `realm:${realm}`);
376
}
377
} catch (err) {
378
extHostLogService.error('ProxyResolver#lookupProxyAuthorization Basic authentication failed', err);
379
}
380
}
381
return undefined;
382
}
383
384
type ProxyAuthenticationClassification = {
385
owner: 'chrmarti';
386
comment: 'Data about proxy authentication requests';
387
authenticationType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Type of the authentication requested' };
388
extensionHostType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Type of the extension host' };
389
};
390
391
type ProxyAuthenticationEvent = {
392
authenticationType: string;
393
extensionHostType: string;
394
};
395
396
let telemetrySent = false;
397
const enableProxyAuthenticationTelemetry = false;
398
function sendTelemetry(mainThreadTelemetry: MainThreadTelemetryShape, authenticate: string[], isRemote: boolean) {
399
if (!enableProxyAuthenticationTelemetry || telemetrySent || !authenticate.length) {
400
return;
401
}
402
telemetrySent = true;
403
404
mainThreadTelemetry.$publicLog2<ProxyAuthenticationEvent, ProxyAuthenticationClassification>('proxyAuthenticationRequest', {
405
authenticationType: authenticate.map(a => a.split(' ')[0]).join(','),
406
extensionHostType: isRemote ? 'remote' : 'local',
407
});
408
}
409
410
function getExtHostConfigValue<T>(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string, fallback: T): T;
411
function getExtHostConfigValue<T>(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string): T | undefined;
412
function getExtHostConfigValue<T>(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string, fallback?: T): T | undefined {
413
if (isRemote) {
414
return configProvider.getConfiguration().get<T>(key) ?? fallback;
415
}
416
const values: ConfigurationInspect<T> | undefined = configProvider.getConfiguration().inspect<T>(key);
417
return values?.globalLocalValue ?? values?.defaultValue ?? fallback;
418
}
419
420