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 3c66dab
Show file tree
Hide file tree
Showing 7 changed files with 116 additions and 19 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
65 changes: 62 additions & 3 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,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<typeof tls.createSecureContext> {
const context = original.apply(null, arguments as any);
Expand Down Expand Up @@ -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();
}
Expand Down
4 changes: 2 additions & 2 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 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);
}
});
});
});
14 changes: 14 additions & 0 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')),
];

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 3c66dab

Please sign in to comment.