diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e9efcf..abcd4c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ # Change Log Notable changes will be documented here. +## [0.14.0] +- Load certificates in tls.connect ([microsoft/vscode#185098](https://github.com/microsoft/vscode/issues/185098)) + +## [0.13.0] +- Rename to @vscode/proxy-agent. + ## [0.12.0] - Avoid buffer deprecation warning (fixes [microsoft/vscode#136874](https://github.com/microsoft/vscode/issues/136874)) diff --git a/package.json b/package.json index eca44c7..776fde8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@vscode/proxy-agent", - "version": "0.13.2", + "version": "0.14.0", "description": "NodeJS http(s) agent implementation for VS Code", "main": "out/index.js", "types": "out/index.d.ts", diff --git a/src/index.ts b/src/index.ts index 1741080..8bc761d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as net from 'net'; import * as http from 'http'; import type * as https from 'https'; import * as tls from 'tls'; @@ -55,6 +56,8 @@ export interface ProxyAgentParams { getLogLevel(): LogLevel; proxyResolveTelemetry(event: ProxyResolveEvent): void; useHostProxy: boolean; + useSystemCertificatesV2: boolean; + addCertificates: (string | Buffer)[]; env: NodeJS.ProcessEnv; } @@ -339,15 +342,71 @@ export function createHttpPatch(originals: typeof http | typeof https, resolvePr } export interface SecureContextOptionsPatch { - _vscodeAdditionalCaCerts?: string[]; + _vscodeAdditionalCaCerts?: (string | Buffer)[]; } -export function createTlsPatch(originals: typeof tls) { +export function createTlsPatch(params: ProxyAgentParams, originals: typeof tls) { return { + connect: patchConnect(params, originals.connect), createSecureContext: patchCreateSecureContext(originals.createSecureContext), }; } +function patchConnect(params: ProxyAgentParams, original: typeof tls.connect): typeof tls.connect { + function connect(options: tls.ConnectionOptions, secureConnectListener?: () => void): tls.TLSSocket; + function connect(port: number, host?: string, options?: tls.ConnectionOptions, secureConnectListener?: () => void): tls.TLSSocket; + function connect(port: number, options?: tls.ConnectionOptions, secureConnectListener?: () => void): tls.TLSSocket; + function connect(...args: any[]): tls.TLSSocket { + let options: tls.ConnectionOptions | undefined = args.find(arg => arg && typeof arg === 'object'); + if (!params.useSystemCertificatesV2 || options?.ca) { + return original.apply(null, arguments as any); + } + params.log(LogLevel.Trace, 'ProxyResolver#connect', ...args); + let secureConnectListener: (() => void) | undefined = args.find(arg => typeof arg === 'function'); + if (!options) { + options = {}; + const listenerIndex = args.findIndex(arg => typeof arg === 'function'); + if (listenerIndex !== -1) { + args[listenerIndex - 1] = options; + } else { + args[2] = options; + } + } else { + options = { + ...options + }; + } + const port = typeof args[0] === 'number' ? args[0] + : typeof args[0] === 'string' && !isNaN(Number(args[0])) ? Number(args[0]) // E.g., http2 module passes port as string. + : options.port!; + const host = typeof args[1] === 'string' ? args[1] : options.host!; + if (!options.socket) { + if (!options.secureContext) { + options.secureContext = tls.createSecureContext(options); + } + const socket = options.socket = new net.Socket(); + getCaCertificates(params) + .then(caCertificates => { + for (const cert of (caCertificates?.certs || []).concat(params.addCertificates)) { + options!.secureContext!.context.addCACert(cert); + } + socket.connect(port, host); + }) + .catch(err => { + params.log(LogLevel.Error, 'ProxyResolver#connect', toErrorMessage(err)); + }); + } + if (typeof args[1] === 'string') { + return original(port, host, options, secureConnectListener); + } else if (typeof args[0] === 'number' || typeof args[0] === 'string' && !isNaN(Number(args[0]))) { + return original(port, options, secureConnectListener); + } else { + return original(options, secureConnectListener); + } + } + return connect; +} + function patchCreateSecureContext(original: typeof tls.createSecureContext): typeof tls.createSecureContext { return function (details?: tls.SecureContextOptions): ReturnType { const context = original.apply(null, arguments as any); @@ -398,7 +457,7 @@ async function getCaCertificates({ log }: ProxyAgentParams) { return _caCertificates; } -async function readCaCertificates() { +async function readCaCertificates(): Promise<{ append: boolean; certs: (string | Buffer)[] } | undefined> { if (process.platform === 'win32') { return readWindowsCaCertificates(); } diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index 9ed0ac2..245ec97 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -2,7 +2,7 @@ version: '3.3' services: test-direct-client: - image: node:14 + image: node:16 links: - test-https-server volumes: @@ -14,7 +14,7 @@ services: - MOCHA_TESTS=src/direct.test.ts src/tls.test.ts command: npm run test:watch test-proxy-client: - image: node:14 + image: node:16 links: - test-http-proxy volumes: diff --git a/tests/test-client/src/direct.test.ts b/tests/test-client/src/direct.test.ts index 8dc39f8..bdd8edd 100644 --- a/tests/test-client/src/direct.test.ts +++ b/tests/test-client/src/direct.test.ts @@ -1,7 +1,7 @@ import * as https from 'https'; import * as vpa from '../../..'; import createPacProxyAgent from '../../../src/agent'; -import { testRequest, ca } from './utils'; +import { testRequest, ca, directProxyAgentParams } from './utils'; import * as assert from 'assert'; describe('Direct client', function () { @@ -63,15 +63,6 @@ describe('Direct client', function () { }); }); - const directProxyAgentParams = { - resolveProxy: async () => 'DIRECT', - getHttpProxySetting: () => undefined, - log: (level: vpa.LogLevel, message: string, ...args: any[]) => level >= vpa.LogLevel.Debug && console.log(message, ...args), - getLogLevel: () => vpa.LogLevel.Debug, - proxyResolveTelemetry: () => undefined, - useHostProxy: true, - env: {}, - }; it('should override original agent', async function () { // https://github.com/microsoft/vscode/issues/117054 const resolveProxy = vpa.createProxyResolver(directProxyAgentParams); diff --git a/tests/test-client/src/tls.test.ts b/tests/test-client/src/tls.test.ts index 0df8564..4491757 100644 --- a/tests/test-client/src/tls.test.ts +++ b/tests/test-client/src/tls.test.ts @@ -1,12 +1,16 @@ import * as tls from 'tls'; import { createTlsPatch, SecureContextOptionsPatch } from '../../../src/index'; -import { ca } from './utils'; +import { ca, directProxyAgentParams } from './utils'; describe('TLS patch', function () { - it('should work without CA option', function (done) { + it('should work without CA option v1', function (done) { const tlsPatched = { ...tls, - ...createTlsPatch(tls), + ...createTlsPatch({ + ...directProxyAgentParams, + useSystemCertificatesV2: false, + addCertificates: [], + }, tls), }; const options: tls.ConnectionOptions = { host: 'test-https-server', @@ -27,4 +31,27 @@ describe('TLS patch', function () { } }); }); + + it('should work without CA option v2', function (done) { + const tlsPatched = { + ...tls, + ...createTlsPatch(directProxyAgentParams, tls), + }; + const options: tls.ConnectionOptions = { + host: 'test-https-server', + port: 443, + servername: 'test-https-server', // for SNI + }; + const socket = tlsPatched.connect(options); + socket.on('error', done); + socket.on('secureConnect', () => { + const { authorized, authorizationError } = socket; + socket.destroy(); + if (authorized) { + done(); + } else { + done(authorizationError); + } + }); + }); }); diff --git a/tests/test-client/src/utils.ts b/tests/test-client/src/utils.ts index 879dc94..2f3cd2f 100644 --- a/tests/test-client/src/utils.ts +++ b/tests/test-client/src/utils.ts @@ -4,11 +4,25 @@ import * as fs from 'fs'; import * as path from 'path'; import * as assert from 'assert'; +import * as vpa from '../../..'; + export const ca = [ fs.readFileSync(path.join(__dirname, '../../test-https-server/ssl_cert.pem')), fs.readFileSync(path.join(__dirname, '../../test-https-server/ssl_teapot_cert.pem')), ]; +export const directProxyAgentParams: vpa.ProxyAgentParams = { + resolveProxy: async () => 'DIRECT', + getHttpProxySetting: () => undefined, + log: (level: vpa.LogLevel, message: string, ...args: any[]) => level >= vpa.LogLevel.Debug && console.log(message, ...args), + getLogLevel: () => vpa.LogLevel.Debug, + proxyResolveTelemetry: () => undefined, + useHostProxy: true, + useSystemCertificatesV2: true, + addCertificates: ca, + env: {}, +}; + export async function testRequest(client: C, options: C extends typeof https ? https.RequestOptions : http.RequestOptions, testOptions: { assertResult?: (result: any) => void; } = {}) { return new Promise((resolve, reject) => { const req = client.request(options, res => {