mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-25 04:36:23 +00:00
419 lines
19 KiB
TypeScript
419 lines
19 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { IExtHostWorkspaceProvider } from '../common/extHostWorkspace.js';
|
|
import { ConfigurationInspect, ExtHostConfigProvider } from '../common/extHostConfiguration.js';
|
|
import { MainThreadTelemetryShape } from '../common/extHost.protocol.js';
|
|
import { IExtensionHostInitData } from '../../services/extensions/common/extensionHostProtocol.js';
|
|
import { ExtHostExtensionService } from './extHostExtensionService.js';
|
|
import { URI } from '../../../base/common/uri.js';
|
|
import { ILogService, LogLevel as LogServiceLevel } from '../../../platform/log/common/log.js';
|
|
import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js';
|
|
import { LogLevel, createHttpPatch, createProxyResolver, createTlsPatch, ProxySupportSetting, ProxyAgentParams, createNetPatch, loadSystemCertificates, ResolveProxyWithRequest } from '@vscode/proxy-agent';
|
|
import { AuthInfo } from '../../../platform/request/common/request.js';
|
|
import { DisposableStore } from '../../../base/common/lifecycle.js';
|
|
import { createRequire } from 'node:module';
|
|
import type * as undiciType from 'undici-types';
|
|
import type * as tlsType from 'tls';
|
|
import { lookupKerberosAuthorization } from '../../../platform/request/node/requestService.js';
|
|
import * as proxyAgent from '@vscode/proxy-agent';
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const http = require('http');
|
|
const https = require('https');
|
|
const tls: typeof tlsType = require('tls');
|
|
const net = require('net');
|
|
|
|
const systemCertificatesV2Default = false;
|
|
const useElectronFetchDefault = false;
|
|
|
|
export function connectProxyResolver(
|
|
extHostWorkspace: IExtHostWorkspaceProvider,
|
|
configProvider: ExtHostConfigProvider,
|
|
extensionService: ExtHostExtensionService,
|
|
extHostLogService: ILogService,
|
|
mainThreadTelemetry: MainThreadTelemetryShape,
|
|
initData: IExtensionHostInitData,
|
|
disposables: DisposableStore,
|
|
) {
|
|
|
|
const isRemote = initData.remote.isRemote;
|
|
const useHostProxyDefault = initData.environment.useHostProxy ?? !isRemote;
|
|
const fallbackToLocalKerberos = useHostProxyDefault;
|
|
const loadLocalCertificates = useHostProxyDefault;
|
|
const isUseHostProxyEnabled = () => configProvider.getConfiguration('http').get<boolean>('useLocalProxyConfiguration', useHostProxyDefault);
|
|
const params: ProxyAgentParams = {
|
|
resolveProxy: url => extHostWorkspace.resolveProxy(url),
|
|
lookupProxyAuthorization: lookupProxyAuthorization.bind(undefined, extHostWorkspace, extHostLogService, mainThreadTelemetry, configProvider, {}, {}, initData.remote.isRemote, fallbackToLocalKerberos),
|
|
getProxyURL: () => getExtHostConfigValue<string>(configProvider, isRemote, 'http.proxy'),
|
|
getProxySupport: () => getExtHostConfigValue<ProxySupportSetting>(configProvider, isRemote, 'http.proxySupport') || 'off',
|
|
getNoProxyConfig: () => getExtHostConfigValue<string[]>(configProvider, isRemote, 'http.noProxy') || [],
|
|
isAdditionalFetchSupportEnabled: () => getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.fetchAdditionalSupport', true),
|
|
addCertificatesV1: () => certSettingV1(configProvider, isRemote),
|
|
addCertificatesV2: () => certSettingV2(configProvider, isRemote),
|
|
log: extHostLogService,
|
|
getLogLevel: () => {
|
|
const level = extHostLogService.getLevel();
|
|
switch (level) {
|
|
case LogServiceLevel.Trace: return LogLevel.Trace;
|
|
case LogServiceLevel.Debug: return LogLevel.Debug;
|
|
case LogServiceLevel.Info: return LogLevel.Info;
|
|
case LogServiceLevel.Warning: return LogLevel.Warning;
|
|
case LogServiceLevel.Error: return LogLevel.Error;
|
|
case LogServiceLevel.Off: return LogLevel.Off;
|
|
default: return never(level);
|
|
}
|
|
function never(level: never) {
|
|
extHostLogService.error('Unknown log level', level);
|
|
return LogLevel.Debug;
|
|
}
|
|
},
|
|
proxyResolveTelemetry: () => { },
|
|
useHostProxy: isUseHostProxyEnabled(), // TODO: can change at runtime now
|
|
loadAdditionalCertificates: async () => {
|
|
const promises: Promise<string[]>[] = [];
|
|
if (initData.remote.isRemote) {
|
|
promises.push(loadSystemCertificates({ log: extHostLogService }));
|
|
}
|
|
if (loadLocalCertificates) {
|
|
extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading certificates from main process');
|
|
const certs = extHostWorkspace.loadCertificates(); // Loading from main process to share cache.
|
|
certs.then(certs => extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loaded certificates from main process', certs.length));
|
|
promises.push(certs);
|
|
}
|
|
// Using https.globalAgent because it is shared with proxy.test.ts and mutable.
|
|
if (initData.environment.extensionTestsLocationURI && (https.globalAgent as any).testCertificates?.length) {
|
|
extHostLogService.trace('ProxyResolver#loadAdditionalCertificates: Loading test certificates');
|
|
promises.push(Promise.resolve((https.globalAgent as any).testCertificates as string[]));
|
|
}
|
|
return (await Promise.all(promises)).flat();
|
|
},
|
|
env: process.env,
|
|
};
|
|
const { resolveProxyWithRequest, resolveProxyURL } = createProxyResolver(params);
|
|
const target = (proxyAgent as any).default || proxyAgent;
|
|
target.resolveProxyURL = resolveProxyURL;
|
|
|
|
patchGlobalFetch(params, configProvider, mainThreadTelemetry, initData, resolveProxyURL, disposables);
|
|
|
|
const lookup = createPatchedModules(params, resolveProxyWithRequest);
|
|
return configureModuleLoading(extensionService, lookup);
|
|
}
|
|
|
|
const unsafeHeaders = [
|
|
'content-length',
|
|
'host',
|
|
'trailer',
|
|
'te',
|
|
'upgrade',
|
|
'cookie2',
|
|
'keep-alive',
|
|
'transfer-encoding',
|
|
'set-cookie',
|
|
];
|
|
|
|
function patchGlobalFetch(params: ProxyAgentParams, configProvider: ExtHostConfigProvider, mainThreadTelemetry: MainThreadTelemetryShape, initData: IExtensionHostInitData, resolveProxyURL: (url: string) => Promise<string | undefined>, disposables: DisposableStore) {
|
|
if (!(globalThis as any).__vscodeOriginalFetch) {
|
|
const originalFetch = globalThis.fetch;
|
|
(globalThis as any).__vscodeOriginalFetch = originalFetch;
|
|
const patchedFetch = proxyAgent.createFetchPatch(params, originalFetch, resolveProxyURL);
|
|
(globalThis as any).__vscodePatchedFetch = patchedFetch;
|
|
let useElectronFetch = false;
|
|
if (!initData.remote.isRemote) {
|
|
useElectronFetch = configProvider.getConfiguration('http').get<boolean>('electronFetch', useElectronFetchDefault);
|
|
disposables.add(configProvider.onDidChangeConfiguration(e => {
|
|
if (e.affectsConfiguration('http.electronFetch')) {
|
|
useElectronFetch = configProvider.getConfiguration('http').get<boolean>('electronFetch', useElectronFetchDefault);
|
|
}
|
|
}));
|
|
}
|
|
// https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
|
|
globalThis.fetch = async function fetch(input: string | URL | Request, init?: RequestInit) {
|
|
function getRequestProperty(name: keyof Request & keyof RequestInit) {
|
|
return init && name in init ? init[name] : typeof input === 'object' && 'cache' in input ? input[name] : undefined;
|
|
}
|
|
// Limitations: https://github.com/electron/electron/pull/36733#issuecomment-1405615494
|
|
// net.fetch fails on manual redirect: https://github.com/electron/electron/issues/43715
|
|
const urlString = typeof input === 'string' ? input : 'cache' in input ? input.url : input.toString();
|
|
const isDataUrl = urlString.startsWith('data:');
|
|
if (isDataUrl) {
|
|
recordFetchFeatureUse(mainThreadTelemetry, 'data');
|
|
}
|
|
const isBlobUrl = urlString.startsWith('blob:');
|
|
if (isBlobUrl) {
|
|
recordFetchFeatureUse(mainThreadTelemetry, 'blob');
|
|
}
|
|
const isManualRedirect = getRequestProperty('redirect') === 'manual';
|
|
if (isManualRedirect) {
|
|
recordFetchFeatureUse(mainThreadTelemetry, 'manualRedirect');
|
|
}
|
|
const integrity = getRequestProperty('integrity');
|
|
if (integrity) {
|
|
recordFetchFeatureUse(mainThreadTelemetry, 'integrity');
|
|
}
|
|
if (!useElectronFetch || isDataUrl || isBlobUrl || isManualRedirect || integrity) {
|
|
const response = await patchedFetch(input, init);
|
|
monitorResponseProperties(mainThreadTelemetry, response, urlString);
|
|
return response;
|
|
}
|
|
// Unsupported headers: https://source.chromium.org/chromium/chromium/src/+/main:services/network/public/cpp/header_util.cc;l=32;drc=ee7299f8961a1b05a3554efcc496b6daa0d7f6e1
|
|
if (init?.headers) {
|
|
const headers = new Headers(init.headers);
|
|
for (const header of unsafeHeaders) {
|
|
headers.delete(header);
|
|
}
|
|
init = { ...init, headers };
|
|
}
|
|
// Support for URL: https://github.com/electron/electron/issues/43712
|
|
const electronInput = input instanceof URL ? input.toString() : input;
|
|
const electron = require('electron');
|
|
const response = await electron.net.fetch(electronInput, init);
|
|
monitorResponseProperties(mainThreadTelemetry, response, urlString);
|
|
return response;
|
|
};
|
|
}
|
|
}
|
|
|
|
function monitorResponseProperties(mainThreadTelemetry: MainThreadTelemetryShape, response: Response, urlString: string) {
|
|
const originalUrl = response.url;
|
|
Object.defineProperty(response, 'url', {
|
|
get() {
|
|
recordFetchFeatureUse(mainThreadTelemetry, 'url');
|
|
return originalUrl || urlString;
|
|
}
|
|
});
|
|
const originalType = response.type;
|
|
Object.defineProperty(response, 'type', {
|
|
get() {
|
|
recordFetchFeatureUse(mainThreadTelemetry, 'typeProperty');
|
|
return originalType !== 'default' ? originalType : 'basic';
|
|
}
|
|
});
|
|
}
|
|
|
|
type FetchFeatureUseClassification = {
|
|
owner: 'chrmarti';
|
|
comment: 'Data about fetch API use';
|
|
url: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the url property was used.' };
|
|
typeProperty: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the type property was used.' };
|
|
data: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a data URL was used.' };
|
|
blob: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a blob URL was used.' };
|
|
integrity: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the integrity property was used.' };
|
|
manualRedirect: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether a manual redirect was used.' };
|
|
};
|
|
|
|
type FetchFeatureUseEvent = {
|
|
url: number;
|
|
typeProperty: number;
|
|
data: number;
|
|
blob: number;
|
|
integrity: number;
|
|
manualRedirect: number;
|
|
};
|
|
|
|
const fetchFeatureUse: FetchFeatureUseEvent = {
|
|
url: 0,
|
|
typeProperty: 0,
|
|
data: 0,
|
|
blob: 0,
|
|
integrity: 0,
|
|
manualRedirect: 0,
|
|
};
|
|
|
|
let timer: NodeJS.Timeout | undefined;
|
|
|
|
function recordFetchFeatureUse(mainThreadTelemetry: MainThreadTelemetryShape, feature: keyof typeof fetchFeatureUse) {
|
|
if (!fetchFeatureUse[feature]++) {
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
}
|
|
timer = setTimeout(() => {
|
|
mainThreadTelemetry.$publicLog2<FetchFeatureUseEvent, FetchFeatureUseClassification>('fetchFeatureUse', fetchFeatureUse);
|
|
}, 10000); // collect additional features for 10 seconds
|
|
timer.unref();
|
|
}
|
|
}
|
|
|
|
function createPatchedModules(params: ProxyAgentParams, resolveProxy: ResolveProxyWithRequest) {
|
|
|
|
function mergeModules(module: any, patch: any) {
|
|
const target = module.default || module;
|
|
target.__vscodeOriginal = Object.assign({}, target);
|
|
return Object.assign(target, patch);
|
|
}
|
|
|
|
return {
|
|
http: mergeModules(http, createHttpPatch(params, http, resolveProxy)),
|
|
https: mergeModules(https, createHttpPatch(params, https, resolveProxy)),
|
|
net: mergeModules(net, createNetPatch(params, net)),
|
|
tls: mergeModules(tls, createTlsPatch(params, tls))
|
|
};
|
|
}
|
|
|
|
function certSettingV1(configProvider: ExtHostConfigProvider, isRemote: boolean) {
|
|
return !getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.experimental.systemCertificatesV2', systemCertificatesV2Default) && !!getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.systemCertificates');
|
|
}
|
|
|
|
function certSettingV2(configProvider: ExtHostConfigProvider, isRemote: boolean) {
|
|
return !!getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.experimental.systemCertificatesV2', systemCertificatesV2Default) && !!getExtHostConfigValue<boolean>(configProvider, isRemote, 'http.systemCertificates');
|
|
}
|
|
|
|
const modulesCache = new Map<IExtensionDescription | undefined, { http?: typeof http; https?: typeof https; undici?: typeof undiciType }>();
|
|
function configureModuleLoading(extensionService: ExtHostExtensionService, lookup: ReturnType<typeof createPatchedModules>): Promise<void> {
|
|
return extensionService.getExtensionPathIndex()
|
|
.then(extensionPaths => {
|
|
const node_module = require('module');
|
|
const original = node_module._load;
|
|
node_module._load = function load(request: string, parent: { filename: string }, isMain: boolean) {
|
|
if (request === 'net') {
|
|
return lookup.net;
|
|
}
|
|
|
|
if (request === 'tls') {
|
|
return lookup.tls;
|
|
}
|
|
|
|
if (request !== 'http' && request !== 'https' && request !== 'undici') {
|
|
return original.apply(this, arguments);
|
|
}
|
|
|
|
const ext = extensionPaths.findSubstr(URI.file(parent.filename));
|
|
let cache = modulesCache.get(ext);
|
|
if (!cache) {
|
|
modulesCache.set(ext, cache = {});
|
|
}
|
|
if (!cache[request]) {
|
|
if (request === 'undici') {
|
|
const undici = original.apply(this, arguments);
|
|
proxyAgent.patchUndici(undici);
|
|
cache[request] = undici;
|
|
} else {
|
|
const mod = lookup[request];
|
|
cache[request] = <any>{ ...mod }; // Copy to work around #93167.
|
|
}
|
|
}
|
|
return cache[request];
|
|
};
|
|
});
|
|
}
|
|
|
|
async function lookupProxyAuthorization(
|
|
extHostWorkspace: IExtHostWorkspaceProvider,
|
|
extHostLogService: ILogService,
|
|
mainThreadTelemetry: MainThreadTelemetryShape,
|
|
configProvider: ExtHostConfigProvider,
|
|
proxyAuthenticateCache: Record<string, string | string[] | undefined>,
|
|
basicAuthCache: Record<string, string | undefined>,
|
|
isRemote: boolean,
|
|
fallbackToLocalKerberos: boolean,
|
|
proxyURL: string,
|
|
proxyAuthenticate: string | string[] | undefined,
|
|
state: { kerberosRequested?: boolean; basicAuthCacheUsed?: boolean; basicAuthAttempt?: number }
|
|
): Promise<string | undefined> {
|
|
const cached = proxyAuthenticateCache[proxyURL];
|
|
if (proxyAuthenticate) {
|
|
proxyAuthenticateCache[proxyURL] = proxyAuthenticate;
|
|
}
|
|
extHostLogService.trace('ProxyResolver#lookupProxyAuthorization callback', `proxyURL:${proxyURL}`, `proxyAuthenticate:${proxyAuthenticate}`, `proxyAuthenticateCache:${cached}`);
|
|
const header = proxyAuthenticate || cached;
|
|
const authenticate = Array.isArray(header) ? header : typeof header === 'string' ? [header] : [];
|
|
sendTelemetry(mainThreadTelemetry, authenticate, isRemote);
|
|
if (authenticate.some(a => /^(Negotiate|Kerberos)( |$)/i.test(a)) && !state.kerberosRequested) {
|
|
state.kerberosRequested = true;
|
|
|
|
try {
|
|
const spnConfig = getExtHostConfigValue<string>(configProvider, isRemote, 'http.proxyKerberosServicePrincipal');
|
|
const response = await lookupKerberosAuthorization(proxyURL, spnConfig, extHostLogService, 'ProxyResolver#lookupProxyAuthorization');
|
|
return 'Negotiate ' + response;
|
|
} catch (err) {
|
|
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication failed', err);
|
|
}
|
|
|
|
if (isRemote && fallbackToLocalKerberos) {
|
|
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Kerberos authentication lookup on host', `proxyURL:${proxyURL}`);
|
|
const auth = await extHostWorkspace.lookupKerberosAuthorization(proxyURL);
|
|
if (auth) {
|
|
return 'Negotiate ' + auth;
|
|
}
|
|
}
|
|
}
|
|
const basicAuthHeader = authenticate.find(a => /^Basic( |$)/i.test(a));
|
|
if (basicAuthHeader) {
|
|
try {
|
|
const cachedAuth = basicAuthCache[proxyURL];
|
|
if (cachedAuth) {
|
|
if (state.basicAuthCacheUsed) {
|
|
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication deleting cached credentials', `proxyURL:${proxyURL}`);
|
|
delete basicAuthCache[proxyURL];
|
|
} else {
|
|
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication using cached credentials', `proxyURL:${proxyURL}`);
|
|
state.basicAuthCacheUsed = true;
|
|
return cachedAuth;
|
|
}
|
|
}
|
|
state.basicAuthAttempt = (state.basicAuthAttempt || 0) + 1;
|
|
const realm = / realm="([^"]+)"/i.exec(basicAuthHeader)?.[1];
|
|
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication lookup', `proxyURL:${proxyURL}`, `realm:${realm}`);
|
|
const url = new URL(proxyURL);
|
|
const authInfo: AuthInfo = {
|
|
scheme: 'basic',
|
|
host: url.hostname,
|
|
port: Number(url.port),
|
|
realm: realm || '',
|
|
isProxy: true,
|
|
attempt: state.basicAuthAttempt,
|
|
};
|
|
const credentials = await extHostWorkspace.lookupAuthorization(authInfo);
|
|
if (credentials) {
|
|
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication received credentials', `proxyURL:${proxyURL}`, `realm:${realm}`);
|
|
const auth = 'Basic ' + Buffer.from(`${credentials.username}:${credentials.password}`).toString('base64');
|
|
basicAuthCache[proxyURL] = auth;
|
|
return auth;
|
|
} else {
|
|
extHostLogService.debug('ProxyResolver#lookupProxyAuthorization Basic authentication received no credentials', `proxyURL:${proxyURL}`, `realm:${realm}`);
|
|
}
|
|
} catch (err) {
|
|
extHostLogService.error('ProxyResolver#lookupProxyAuthorization Basic authentication failed', err);
|
|
}
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
type ProxyAuthenticationClassification = {
|
|
owner: 'chrmarti';
|
|
comment: 'Data about proxy authentication requests';
|
|
authenticationType: { classification: 'PublicNonPersonalData'; purpose: 'FeatureInsight'; comment: 'Type of the authentication requested' };
|
|
extensionHostType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Type of the extension host' };
|
|
};
|
|
|
|
type ProxyAuthenticationEvent = {
|
|
authenticationType: string;
|
|
extensionHostType: string;
|
|
};
|
|
|
|
let telemetrySent = false;
|
|
|
|
function sendTelemetry(mainThreadTelemetry: MainThreadTelemetryShape, authenticate: string[], isRemote: boolean) {
|
|
if (telemetrySent || !authenticate.length) {
|
|
return;
|
|
}
|
|
telemetrySent = true;
|
|
|
|
mainThreadTelemetry.$publicLog2<ProxyAuthenticationEvent, ProxyAuthenticationClassification>('proxyAuthenticationRequest', {
|
|
authenticationType: authenticate.map(a => a.split(' ')[0]).join(','),
|
|
extensionHostType: isRemote ? 'remote' : 'local',
|
|
});
|
|
}
|
|
|
|
function getExtHostConfigValue<T>(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string, fallback: T): T;
|
|
function getExtHostConfigValue<T>(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string): T | undefined;
|
|
function getExtHostConfigValue<T>(configProvider: ExtHostConfigProvider, isRemote: boolean, key: string, fallback?: T): T | undefined {
|
|
if (isRemote) {
|
|
return configProvider.getConfiguration().get<T>(key) ?? fallback;
|
|
}
|
|
const values: ConfigurationInspect<T> | undefined = configProvider.getConfiguration().inspect<T>(key);
|
|
return values?.globalLocalValue ?? values?.defaultValue ?? fallback;
|
|
}
|