Skip to content

Commit

Permalink
Load certificates in tls.connect (microsoft/vscode#185098)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrmarti committed Jun 14, 2023
1 parent b9db0d1 commit 84a07b0
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 34 deletions.
78 changes: 71 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -55,6 +56,8 @@ export interface ProxyAgentParams {
getLogLevel(): LogLevel;
proxyResolveTelemetry(event: ProxyResolveEvent): void;
useHostProxy: boolean;
useSystemCertificatesV2: boolean;
addCertificates: (string | Buffer)[];
env: NodeJS.ProcessEnv;
}

Expand Down Expand Up @@ -339,15 +342,73 @@ 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 => {
if (caCertificates) {
for (const cert of caCertificates.certs) {
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<typeof tls.createSecureContext> {
const context = original.apply(null, arguments as any);
Expand Down Expand Up @@ -383,22 +444,25 @@ function useSystemCertificates(params: ProxyAgentParams, useSystemCertificates:
}

let _caCertificates: ReturnType<typeof readCaCertificates> | Promise<undefined>;
async function getCaCertificates({ log }: ProxyAgentParams) {
async function getCaCertificates(params: ProxyAgentParams) {
if (!_caCertificates) {
_caCertificates = readCaCertificates()
.then(res => {
log(LogLevel.Debug, 'ProxyResolver#getCaCertificates count', res && res.certs.length);
return res && res.certs.length ? res : undefined;
params.log(LogLevel.Debug, 'ProxyResolver#getCaCertificates count', res && res.certs.length);
if (res?.certs.length || params.addCertificates.length) {
return res ? { append: res.append, certs: res.certs.concat(params.addCertificates) } : { append: true, certs: params.addCertificates };
}
return undefined;
})
.catch(err => {
log(LogLevel.Error, 'ProxyResolver#getCaCertificates error', toErrorMessage(err));
params.log(LogLevel.Error, 'ProxyResolver#getCaCertificates error', toErrorMessage(err));
return undefined;
});
}
return _caCertificates;
}

async function readCaCertificates() {
async function readCaCertificates(): Promise<{ append: boolean; certs: (string | Buffer)[] } | undefined> {
if (process.platform === 'win32') {
return readWindowsCaCertificates();
}
Expand Down
24 changes: 12 additions & 12 deletions tests/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ version: '3.3'

services:
test-direct-client:
image: node:14
image: node:16
links:
- test-https-server
volumes:
Expand All @@ -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:
Expand All @@ -34,16 +34,16 @@ services:
- test-proxies-and-servers
ports:
- 3128
test-http-auth-proxy:
image: test-http-auth-proxy:latest
build: test-http-auth-proxy
links:
- test-https-server
networks:
- test-proxies
- test-proxies-and-servers
ports:
- 3128
# test-http-auth-proxy:
# image: test-http-auth-proxy:latest
# build: test-http-auth-proxy
# links:
# - test-https-server
# networks:
# - test-proxies
# - test-proxies-and-servers
# ports:
# - 3128
test-https-server:
image: test-https-server:latest
build: test-https-server
Expand Down
11 changes: 1 addition & 10 deletions tests/test-client/src/direct.test.ts
Original file line number Diff line number Diff line change
@@ -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 () {
Expand Down Expand Up @@ -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);
Expand Down
33 changes: 30 additions & 3 deletions tests/test-client/src/tls.test.ts
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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);
}
});
});
});
18 changes: 16 additions & 2 deletions tests/test-client/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
fs.readFileSync(path.join(__dirname, '../../test-https-server/ssl_cert.pem')).toString(),
fs.readFileSync(path.join(__dirname, '../../test-https-server/ssl_teapot_cert.pem')).toString(),
];

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<C extends typeof https | typeof http>(client: C, options: C extends typeof https ? https.RequestOptions : http.RequestOptions, testOptions: { assertResult?: (result: any) => void; } = {}) {
return new Promise<void>((resolve, reject) => {
const req = client.request(options, res => {
Expand Down

0 comments on commit 84a07b0

Please sign in to comment.