Skip to content

Commit 73f5cb8

Browse files
authored
Support loading certificates with Node.js (#80)
1 parent 51694ca commit 73f5cb8

9 files changed

Lines changed: 148 additions & 100 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# Change Log
22
Notable changes will be documented here.
33

4+
## [0.36.0]
5+
- Support loading certificates with Node.js ([microsoft/vscode-proxy-agent#80](https://github.com/microsoft/vscode-proxy-agent/pull/80))
6+
47
## [0.35.0]
58
- Flush proxy cache when network interfaces changed ([microsoft/vscode-proxy-agent#79](https://github.com/microsoft/vscode-proxy-agent/pull/79))
69

package-lock.json

Lines changed: 20 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@vscode/proxy-agent",
3-
"version": "0.35.0",
3+
"version": "0.36.0",
44
"description": "NodeJS http(s) agent implementation for VS Code",
55
"main": "out/index.js",
66
"types": "out/index.d.ts",
@@ -27,6 +27,9 @@
2727
"url": "https://github.com/microsoft/vscode-proxy-agent/issues"
2828
},
2929
"homepage": "https://github.com/microsoft/vscode-proxy-agent",
30+
"engines": {
31+
"node": ">=22.15.0"
32+
},
3033
"dependencies": {
3134
"@tootallnate/once": "^3.0.0",
3235
"agent-base": "^7.0.1",
@@ -38,7 +41,7 @@
3841
},
3942
"devDependencies": {
4043
"@types/debug": "^4.1.9",
41-
"@types/node": "^20.8.4",
44+
"@types/node": "^22.18.10",
4245
"typescript": "^5.2.2"
4346
},
4447
"scripts": {

src/index.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export interface ProxyAgentParams {
7070
isAdditionalFetchSupportEnabled: () => boolean,
7171
addCertificatesV1: () => boolean,
7272
addCertificatesV2: () => boolean,
73+
loadSystemCertificatesFromNode: () => boolean | undefined;
7374
loadAdditionalCertificates(): Promise<string[]>;
7475
lookupProxyAuthorization?: LookupProxyAuthorization;
7576
log: Log;
@@ -522,17 +523,18 @@ function patchTlsConnect(params: ProxyAgentParams, original: typeof tls.connect)
522523
if (!options.secureContext) {
523524
options.secureContext = tls.createSecureContext(options);
524525
}
525-
if (!_certificates) {
526+
const certificates = _certs.get(!!params.loadSystemCertificatesFromNode())?.result;
527+
if (!certificates) {
526528
params.log.trace('ProxyResolver#tls.connect waiting for existing socket connect');
527529
options.socket.once('connect' , () => {
528530
params.log.trace('ProxyResolver#tls.connect got existing socket connect - adding certs');
529-
for (const cert of _certificates || []) {
531+
for (const cert of _certs.get(!!params.loadSystemCertificatesFromNode())?.result || []) {
530532
options!.secureContext!.context.addCACert(cert);
531533
}
532534
});
533535
} else {
534536
params.log.trace('ProxyResolver#tls.connect existing socket already connected - adding certs');
535-
for (const cert of _certificates) {
537+
for (const cert of certificates) {
536538
options!.secureContext!.context.addCACert(cert);
537539
}
538540
}
@@ -877,28 +879,41 @@ function addCertificatesToOptionsV1(params: ProxyAgentParams, addCertificatesV1:
877879
}
878880
}
879881

880-
let _certificatesPromise: Promise<string[]> | undefined;
881-
let _certificates: string[] | undefined;
882+
const _certs = new Map<boolean, { promise: Promise<string[]>; result: string[] | undefined }>();
882883
export async function getOrLoadAdditionalCertificates(params: ProxyAgentParams) {
883-
if (!_certificatesPromise) {
884-
_certificatesPromise = (async () => {
885-
return _certificates = await params.loadAdditionalCertificates();
886-
})();
884+
const loadFromNode = !!params.loadSystemCertificatesFromNode();
885+
if (!_certs.has(loadFromNode)) {
886+
const cert: { promise: Promise<string[]>; result: string[] | undefined } = {
887+
promise: (async () => {
888+
const result = await params.loadAdditionalCertificates();
889+
return cert!.result = result; // need to await before accessing cert.
890+
})(),
891+
result: undefined
892+
};
893+
_certs.set(loadFromNode, cert);
887894
}
888-
return _certificatesPromise;
895+
return _certs.get(loadFromNode)!.promise;
889896
}
890897

891898
export interface CertificateParams {
899+
loadSystemCertificatesFromNode: () => boolean | undefined;
892900
log: Log;
893901
}
894902

895903
let _systemCertificatesPromise: Promise<string[]> | undefined;
896904
export async function loadSystemCertificates(params: CertificateParams) {
905+
if (!!params.loadSystemCertificatesFromNode?.()) { // Checking if function exists for backward compatibility.
906+
const start = Date.now();
907+
const systemCerts = tls.getCACertificates('system');
908+
params.log.debug(`ProxyResolver#loadSystemCertificates from Node.js count (${Date.now() - start}ms)`, systemCerts.length);
909+
return systemCerts;
910+
}
897911
if (!_systemCertificatesPromise) {
898912
_systemCertificatesPromise = (async () => {
899913
try {
914+
const start = Date.now();
900915
const certs = await readSystemCertificates();
901-
params.log.debug('ProxyResolver#loadSystemCertificates count', certs.length);
916+
params.log.debug(`ProxyResolver#loadSystemCertificates count (${Date.now() - start}ms)`, certs.length);
902917
const now = Date.now();
903918
const filtered = certs
904919
.filter(cert => {
@@ -923,8 +938,7 @@ export async function loadSystemCertificates(params: CertificateParams) {
923938
}
924939

925940
export function resetCaches() {
926-
_certificatesPromise = undefined;
927-
_certificates = undefined;
941+
_certs.clear();
928942
_systemCertificatesPromise = undefined;
929943
}
930944

tests/test-client/package-lock.json

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/test-client/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
"devDependencies": {
1414
"@types/kerberos": "^1.1.2",
1515
"@types/mocha": "5.2.5",
16-
"@types/node": "^20.8.4",
16+
"@types/node": "^22.18.10",
1717
"kerberos": "^2.0.1",
1818
"mocha": "10.8.2",
1919
"ts-node": "9.1.1",

tests/test-client/src/direct.test.ts

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -230,30 +230,39 @@ describe('Direct client', function () {
230230
assert.strictEqual(err?.message, 'self-signed certificate');
231231
}
232232
});
233-
it('should use ca agent option 2', async function () {
234-
try {
235-
vpa.resetCaches(); // Allows loadAdditionalCertificates to run again.
236-
const params = {
237-
...directProxyAgentParamsV1,
238-
loadAdditionalCertificates: async () => [
239-
...await vpa.loadSystemCertificates({ log: console }),
240-
],
241-
};
242-
const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(params);
243-
const patchedHttps: typeof https = {
244-
...https,
245-
...vpa.createHttpPatch(params, https, resolveProxy),
246-
} as any;
247-
await testRequest(patchedHttps, {
248-
hostname: 'test-https-server',
249-
path: '/test-path',
250-
_vscodeTestReplaceCaCerts: true,
251-
agent: new https.Agent({ ca }),
252-
});
253-
} finally {
254-
vpa.resetCaches(); // Allows loadAdditionalCertificates to run again.
255-
}
256-
});
233+
for (const loadSystemCertificatesFromNode of [
234+
() => true,
235+
() => false,
236+
undefined as any as (() => boolean), // Test backward compatibility
237+
]) {
238+
it('should use ca agent option 2', async function () {
239+
try {
240+
vpa.resetCaches(); // Allows loadAdditionalCertificates to run again.
241+
const params = {
242+
...directProxyAgentParamsV1,
243+
loadAdditionalCertificates: async () => [
244+
...await vpa.loadSystemCertificates({
245+
loadSystemCertificatesFromNode,
246+
log: console,
247+
}),
248+
],
249+
};
250+
const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(params);
251+
const patchedHttps: typeof https = {
252+
...https,
253+
...vpa.createHttpPatch(params, https, resolveProxy),
254+
} as any;
255+
await testRequest(patchedHttps, {
256+
hostname: 'test-https-server',
257+
path: '/test-path',
258+
_vscodeTestReplaceCaCerts: true,
259+
agent: new https.Agent({ ca }),
260+
});
261+
} finally {
262+
vpa.resetCaches(); // Allows loadAdditionalCertificates to run again.
263+
}
264+
});
265+
}
257266
it('should prefer ca agent option', async function () {
258267
const { resolveProxyWithRequest: resolveProxy } = vpa.createProxyResolver(directProxyAgentParamsV1);
259268
const patchedHttps: typeof https = {

0 commit comments

Comments
 (0)