diff --git a/build/gulpfile.vscode.js b/build/gulpfile.vscode.js index 9c2231119e9..041ceea7419 100644 --- a/build/gulpfile.vscode.js +++ b/build/gulpfile.vscode.js @@ -339,6 +339,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op .pipe(util.cleanNodeModule('node-pty', ['binding.gyp', 'build/**', 'src/**', 'tools/**'], ['build/Release/*.exe', 'build/Release/*.dll', 'build/Release/*.node'])) .pipe(util.cleanNodeModule('vscode-nsfw', ['binding.gyp', 'build/**', 'src/**', 'openpa/**', 'includes/**'], ['build/Release/*.node', '**/*.a'])) .pipe(util.cleanNodeModule('vsda', ['binding.gyp', 'README.md', 'build/**', '*.bat', '*.sh', '*.cpp', '*.h'], ['build/Release/vsda.node'])) + .pipe(util.cleanNodeModule('win-ca-lib', ['**/*'], ['package.json', '**/*.node'])) + .pipe(util.cleanNodeModule('node-addon-api', ['**/*'])) .pipe(createAsar(path.join(process.cwd(), 'node_modules'), ['**/*.node', '**/vscode-ripgrep/bin/*', '**/node-pty/build/Release/*'], 'app/node_modules.asar')); let all = es.merge( diff --git a/package.json b/package.json index 51241ada59a..bdb5c55bd75 100644 --- a/package.json +++ b/package.json @@ -146,8 +146,9 @@ }, "optionalDependencies": { "vscode-windows-registry": "1.0.1", + "win-ca-lib": "https://github.com/chrmarti/win-ca/releases/download/v2.4.1-lib-test/win-ca-lib-2.4.1.tgz", "windows-foreground-love": "0.1.0", "windows-mutex": "0.2.1", "windows-process-tree": "0.2.3" } -} \ No newline at end of file +} diff --git a/src/vs/platform/request/node/request.ts b/src/vs/platform/request/node/request.ts index 8602bb0357c..4e0c6c2b101 100644 --- a/src/vs/platform/request/node/request.ts +++ b/src/vs/platform/request/node/request.ts @@ -58,6 +58,11 @@ Registry.as(Extensions.Configuration) ], default: 'override', description: localize('proxySupport', "Use the proxy support for extensions.") + }, + 'http.systemCertificates': { + type: 'boolean', + default: true, + description: localize('systemCertificates', "Controls whether CA certificates should be loaded from the OS overriding Node.js' built-in CA certificates. Currently only supported on Windows.") } } }); diff --git a/src/vs/workbench/services/extensions/node/proxyResolver.ts b/src/vs/workbench/services/extensions/node/proxyResolver.ts index 1643badcc71..9c6b4880a85 100644 --- a/src/vs/workbench/services/extensions/node/proxyResolver.ts +++ b/src/vs/workbench/services/extensions/node/proxyResolver.ts @@ -5,7 +5,9 @@ import * as http from 'http'; import * as https from 'https'; +import * as tls from 'tls'; import * as nodeurl from 'url'; +import * as os from 'os'; import { assign } from 'vs/base/common/objects'; import { endsWith } from 'vs/base/common/strings'; @@ -118,11 +120,23 @@ function setupProxyResolution( results = []; } - function resolveProxy(req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) { + function resolveProxy(flags: { useProxySettings: boolean, useSystemCertificates: boolean }, req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) { if (!timeout) { timeout = setTimeout(logEvent, 10 * 60 * 1000); } + useSystemCertificates(extHostLogService, flags.useSystemCertificates, opts, () => { + useProxySettings(flags.useProxySettings, req, opts, url, callback); + }); + } + + function useProxySettings(useProxySettings: boolean, req: http.ClientRequest, opts: http.RequestOptions, url: string, callback: (proxy?: string) => void) { + + if (!useProxySettings) { + callback('DIRECT'); + return; + } + const parsedUrl = nodeurl.parse(url); // Coming from Node's URL, sticking with that. const hostname = parsedUrl.hostname; @@ -254,34 +268,43 @@ function noProxyFromEnv(envValue?: string) { } function createPatchedModules(configProvider: ExtHostConfigProvider, resolveProxy: ReturnType) { - const setting = { + const proxySetting = { config: configProvider.getConfiguration('http') .get('proxySupport') || 'off' }; configProvider.onDidChangeConfiguration(e => { - setting.config = configProvider.getConfiguration('http') + proxySetting.config = configProvider.getConfiguration('http') .get('proxySupport') || 'off'; }); + const certSetting = { + config: !!configProvider.getConfiguration('http') + .get('systemCertificates') + }; + configProvider.onDidChangeConfiguration(e => { + certSetting.config = !!configProvider.getConfiguration('http') + .get('systemCertificates'); + }); return { http: { - off: assign({}, http, patches(http, resolveProxy, { config: 'off' }, true)), - on: assign({}, http, patches(http, resolveProxy, { config: 'on' }, true)), - override: assign({}, http, patches(http, resolveProxy, { config: 'override' }, true)), - onRequest: assign({}, http, patches(http, resolveProxy, setting, true)), - default: assign(http, patches(http, resolveProxy, setting, false)) // run last + off: assign({}, http, patches(http, resolveProxy, { config: 'off' }, certSetting, true)), + on: assign({}, http, patches(http, resolveProxy, { config: 'on' }, certSetting, true)), + override: assign({}, http, patches(http, resolveProxy, { config: 'override' }, certSetting, true)), + onRequest: assign({}, http, patches(http, resolveProxy, proxySetting, certSetting, true)), + default: assign(http, patches(http, resolveProxy, proxySetting, certSetting, false)) // run last }, https: { - off: assign({}, https, patches(https, resolveProxy, { config: 'off' }, true)), - on: assign({}, https, patches(https, resolveProxy, { config: 'on' }, true)), - override: assign({}, https, patches(https, resolveProxy, { config: 'override' }, true)), - onRequest: assign({}, https, patches(https, resolveProxy, setting, true)), - default: assign(https, patches(https, resolveProxy, setting, false)) // run last - } + off: assign({}, https, patches(https, resolveProxy, { config: 'off' }, certSetting, true)), + on: assign({}, https, patches(https, resolveProxy, { config: 'on' }, certSetting, true)), + override: assign({}, https, patches(https, resolveProxy, { config: 'override' }, certSetting, true)), + onRequest: assign({}, https, patches(https, resolveProxy, proxySetting, certSetting, true)), + default: assign(https, patches(https, resolveProxy, proxySetting, certSetting, false)) // run last + }, + tls: assign(tls, tlsPatches(tls)) }; } -function patches(originals: typeof http | typeof https, resolveProxy: ReturnType, setting: { config: string; }, onRequest: boolean) { +function patches(originals: typeof http | typeof https, resolveProxy: ReturnType, proxySetting: { config: string }, certSetting: { config: boolean }, onRequest: boolean) { return { get: patch(originals.get), request: patch(originals.request) @@ -300,12 +323,15 @@ function patches(originals: typeof http | typeof https, resolveProxy: ReturnType } options = options || {}; - const config = onRequest && ((options)._vscodeProxySupport || /* LS */ (options)._vscodeSystemProxy) || setting.config; - if (config === 'off') { + if (options.socketPath) { return original.apply(null, arguments as unknown as any[]); } - if (!options.socketPath && (config === 'override' || config === 'on' && !options.agent) && !(options.agent instanceof ProxyAgent)) { + const config = onRequest && ((options)._vscodeProxySupport || /* LS */ (options)._vscodeSystemProxy) || proxySetting.config; + const useProxySettings = (config === 'override' || config === 'on' && !options.agent) && !(options.agent instanceof ProxyAgent); + const useSystemCertificates = certSetting.config && originals === https && !(options as https.RequestOptions).ca; + + if (useProxySettings || useSystemCertificates) { if (url) { const parsed = typeof url === 'string' ? new nodeurl.URL(url) : url; const urlOptions = { @@ -322,7 +348,7 @@ function patches(originals: typeof http | typeof https, resolveProxy: ReturnType options = { ...options }; } options.agent = new ProxyAgent({ - resolveProxy, + resolveProxy: resolveProxy.bind(undefined, { useProxySettings, useSystemCertificates }), defaultPort: originals === https ? 443 : 80, originalAgent: options.agent }); @@ -335,12 +361,35 @@ function patches(originals: typeof http | typeof https, resolveProxy: ReturnType } } +function tlsPatches(originals: typeof tls) { + return { + createSecureContext: patch(originals.createSecureContext) + }; + + function patch(original: typeof tls.createSecureContext): typeof tls.createSecureContext { + return function (details: tls.SecureContextOptions): ReturnType { + const context = original.apply(null, arguments as unknown as any[]); + const cas = (details as any)._vscodeAdditionalCAs; + if (cas) { + for (const ca of cas) { + context.context.addCACert(ca); + } + } + return context; + }; + } +} + function configureModuleLoading(extensionService: ExtHostExtensionService, lookup: ReturnType): Promise { return extensionService.getExtensionPathIndex() .then(extensionPaths => { const node_module = require.__$__nodeRequire('module'); const original = node_module._load; node_module._load = function load(request: string, parent: any, isMain: any) { + if (request === 'tls') { + return lookup.tls; + } + if (request !== 'http' && request !== 'https') { return original.apply(this, arguments); } @@ -353,4 +402,65 @@ function configureModuleLoading(extensionService: ExtHostExtensionService, looku return modules.default; }; }); -} \ No newline at end of file +} + +function useSystemCertificates(extHostLogService: ExtHostLogService, useSystemCertificates: boolean, opts: http.RequestOptions, callback: () => void) { + if (useSystemCertificates) { + getCertificates(extHostLogService) + .then(cas => { + if (cas) { + (opts as any)._vscodeAdditionalCAs = cas; + } + callback(); + }) + .catch(err => { + extHostLogService.error('ProxyResolver#useSystemCertificates', toErrorMessage(err)); + }); + } else { + callback(); + } +} + +let _certificates: Promise; +async function getCertificates(extHostLogService: ExtHostLogService) { + if (!_certificates) { + _certificates = readCertificates() + .catch(err => { + extHostLogService.error('ProxyResolver#getCertificates', toErrorMessage(err)); + return undefined; + }); + } + return _certificates; +} + +async function readCertificates() { + if (process.platform === 'win32') { + const winCA = require.__$__nodeRequire('win-ca-lib'); + + let ders = []; + const store = winCA(); + try { + let der; + while (der = store.next()) { + ders.push(der); + } + } finally { + store.done(); + } + + const seen = {}; + return ders.map(derToPem) + .filter(pem => !seen[pem] && (seen[pem] = true)); + } + return undefined; +} + +function derToPem(blob) { + const lines = ['-----BEGIN CERTIFICATE-----']; + const der = blob.toString('base64'); + for (let i = 0; i < der.length; i += 64) { + lines.push(der.substr(i, 64)); + } + lines.push('-----END CERTIFICATE-----', ''); + return lines.join(os.EOL); +} diff --git a/yarn.lock b/yarn.lock index 53dc4477514..fe110aa20a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5940,6 +5940,11 @@ node-abi@^2.2.0: dependencies: semver "^5.4.1" +node-addon-api@^1.3.0: + version "1.6.2" + resolved "https://registry.yarnpkg.com/node-addon-api/-/node-addon-api-1.6.2.tgz#d8aad9781a5cfc4132cc2fecdbdd982534265217" + integrity sha512-479Bjw9nTE5DdBSZZWprFryHGjUaQC31y1wHo19We/k0BZlrmhqQitWoUL0cD8+scljCbIUL+E58oRDEakdGGA== + node-libs-browser@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-2.1.0.tgz#5f94263d404f6e44767d726901fff05478d600df" @@ -9585,6 +9590,12 @@ wide-align@^1.1.0: dependencies: string-width "^1.0.2 || 2" +"win-ca-lib@https://github.com/chrmarti/win-ca/releases/download/v2.4.1-lib-test/win-ca-lib-2.4.1.tgz": + version "2.4.1" + resolved "https://github.com/chrmarti/win-ca/releases/download/v2.4.1-lib-test/win-ca-lib-2.4.1.tgz#92415b2c45d08b72217011db8050f8c957e208c4" + dependencies: + node-addon-api "^1.3.0" + window-size@0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"