Skip to content

Commit

Permalink
Load certificates in net.connect (microsoft/vscode#185098)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrmarti committed Jun 19, 2023
1 parent 3c66dab commit f903e8a
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 16 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Change Log
Notable changes will be documented here.

## [0.14.1]
- Load certificates in net.connect ([microsoft/vscode#185098](https://github.com/microsoft/vscode/issues/185098))

## [0.14.0]
- Load certificates in tls.connect ([microsoft/vscode#185098](https://github.com/microsoft/vscode/issues/185098))

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.14.0",
"version": "0.14.1",
"description": "NodeJS http(s) agent implementation for VS Code",
"main": "out/index.js",
"types": "out/index.d.ts",
Expand Down
86 changes: 72 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -345,14 +345,42 @@ export interface SecureContextOptionsPatch {
_vscodeAdditionalCaCerts?: (string | Buffer)[];
}

export function createNetPatch(params: ProxyAgentParams, originals: typeof net) {
return {
connect: patchNetConnect(params, originals.connect),
};
}

function patchNetConnect(params: ProxyAgentParams, original: typeof net.connect): typeof net.connect {
function connect(options: net.NetConnectOpts, connectionListener?: () => void): net.Socket;
function connect(port: number, host?: string, connectionListener?: () => void): net.Socket;
function connect(path: string, connectionListener?: () => void): net.Socket;
function connect(...args: any[]): net.Socket {
if (!params.useSystemCertificatesV2) {
return original.apply(null, arguments as any);
}
params.log(LogLevel.Trace, 'ProxyResolver#net.connect', ...args);
const socket = new net.Socket();
getCaCertificates(params)
.then(() => {
socket.connect.apply(socket, arguments as any);
})
.catch(err => {
params.log(LogLevel.Error, 'ProxyResolver#net.connect', toErrorMessage(err));
});
return socket;
}
return connect;
}

export function createTlsPatch(params: ProxyAgentParams, originals: typeof tls) {
return {
connect: patchConnect(params, originals.connect),
connect: patchTlsConnect(params, originals.connect),
createSecureContext: patchCreateSecureContext(originals.createSecureContext),
};
}

function patchConnect(params: ProxyAgentParams, original: typeof tls.connect): typeof tls.connect {
function patchTlsConnect(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;
Expand All @@ -378,28 +406,48 @@ function patchConnect(params: ProxyAgentParams, original: typeof tls.connect): t
}
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) {
: options.port;
const host = typeof args[1] === 'string' ? args[1] : options.host;
if (options.socket) {
if (!options.secureContext) {
options.secureContext = tls.createSecureContext(options);
}
if (!_caCertificateValues) {
params.log(LogLevel.Trace, 'ProxyResolver#connect waiting for existing socket connect');
options.socket.once('connect' , () => {
params.log(LogLevel.Trace, 'ProxyResolver#connect got existing socket connect - adding certs');
for (const cert of _caCertificateValues || []) {
options!.secureContext!.context.addCACert(cert);
}
});
} else {
params.log(LogLevel.Trace, 'ProxyResolver#connect existing socket already connected - adding certs');
for (const cert of _caCertificateValues) {
options!.secureContext!.context.addCACert(cert);
}
}
} else {
if (!options.secureContext) {
options.secureContext = tls.createSecureContext(options);
}
params.log(LogLevel.Trace, 'ProxyResolver#connect creating unconnected socket');
const socket = options.socket = new net.Socket();
getCaCertificates(params)
.then(caCertificates => {
for (const cert of (caCertificates?.certs || []).concat(params.addCertificates)) {
params.log(LogLevel.Trace, 'ProxyResolver#connect adding certs before connecting socket');
for (const cert of caCertificates?.certs || []) {
options!.secureContext!.context.addCACert(cert);
}
socket.connect(port, host);
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);
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);
return original(port!, options, secureConnectListener);
} else {
return original(options, secureConnectListener);
}
Expand Down Expand Up @@ -441,22 +489,32 @@ function useSystemCertificates(params: ProxyAgentParams, useSystemCertificates:
}
}

let _caCertificates: ReturnType<typeof readCaCertificates> | Promise<undefined>;
async function getCaCertificates({ log }: ProxyAgentParams) {
let _caCertificates: ReturnType<typeof readCaCertificates> | Promise<undefined> | undefined;
let _caCertificateValues: (string | Buffer)[] | undefined;
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);
_caCertificateValues = (res?.certs || []).concat(params.addCertificates);
return _caCertificateValues.length > 0 ? {
certs: _caCertificateValues,
append: res?.append !== false,
} : undefined;
})
.catch(err => {
log(LogLevel.Error, 'ProxyResolver#getCaCertificates error', toErrorMessage(err));
params.log(LogLevel.Error, 'ProxyResolver#getCaCertificates error', toErrorMessage(err));
return undefined;
});
}
return _caCertificates;
}

export function resetCaches() {
_caCertificates = undefined;
_caCertificateValues = undefined;
}

async function readCaCertificates(): Promise<{ append: boolean; certs: (string | Buffer)[] } | undefined> {
if (process.platform === 'win32') {
return readWindowsCaCertificates();
Expand Down
62 changes: 61 additions & 1 deletion tests/test-client/src/tls.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import * as net from 'net';
import * as tls from 'tls';
import { createTlsPatch, SecureContextOptionsPatch } from '../../../src/index';
import { createNetPatch, createTlsPatch, resetCaches, SecureContextOptionsPatch } from '../../../src/index';
import { ca, directProxyAgentParams } from './utils';

describe('TLS patch', function () {
beforeEach(() => {
resetCaches();
});
it('should work without CA option v1', function (done) {
const tlsPatched = {
...tls,
Expand Down Expand Up @@ -54,4 +58,60 @@ describe('TLS patch', function () {
}
});
});

it('should work with existing connected socket v2', function (done) {
const netPatched = {
...net,
...createNetPatch(directProxyAgentParams, net),
};
const tlsPatched = {
...tls,
...createTlsPatch(directProxyAgentParams, tls),
};
const existingSocket = netPatched.connect(443, 'test-https-server');
existingSocket.on('connect', () => {
const options: tls.ConnectionOptions = {
socket: existingSocket,
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);
}
});
});
});

it('should work with existing connecting socket v2', function (done) {
const netPatched = {
...net,
...createNetPatch(directProxyAgentParams, net),
};
const tlsPatched = {
...tls,
...createTlsPatch(directProxyAgentParams, tls),
};
const existingSocket = netPatched.connect(443, 'test-https-server');
const options: tls.ConnectionOptions = {
socket: existingSocket,
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);
}
});
});
});

0 comments on commit f903e8a

Please sign in to comment.