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