Skip to content

Commit 1046b54

Browse files
SDK-6180 Support proxyCaCertificate for SSL-inspecting proxies
Trust a customer-provided CA bundle for all outbound HTTPS (axios) for customers behind SSL-inspecting corporate proxies (Zscaler/Netskope). - New bin/helpers/caCertHelper.js: resolves the cert (env BROWSERSTACK_EXTRA_CA_CERTS > browserstack.json connection_settings. proxyCaCertificate or top-level), patches tls.createSecureContext to addCACert it (MERGED with system roots; covers every axios call and the HttpsProxyAgent tunnel in one hook), and sets NODE_EXTRA_CA_CERTS. Never throws. - Hooked in validateBstackJson (runs first in every command, before any axios call). Verified through mitmproxy: with the cert, axios requests succeed through the proxy; without it the CLI still rejects the MITM cert (cert-gated). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bb73e30 commit 1046b54

2 files changed

Lines changed: 77 additions & 0 deletions

File tree

bin/helpers/caCertHelper.js

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use strict';
2+
3+
/*
4+
* SDK-5953: trust a customer-provided CA certificate for SSL-inspecting corporate
5+
* proxies (Zscaler, Netskope, Forcepoint).
6+
*
7+
* Resolution order for the cert path:
8+
* 1. env BROWSERSTACK_EXTRA_CA_CERTS (consistency with the other SDKs)
9+
* 2. browserstack.json connection_settings.proxyCaCertificate
10+
* 3. browserstack.json top-level proxyCaCertificate
11+
*
12+
* The CLI makes outbound HTTPS via axios across many files (some through an
13+
* HttpsProxyAgent when a proxy is set, some direct). Rather than patch every
14+
* axios call, we patch `tls.createSecureContext` once and `addCACert` the
15+
* customer cert — this MERGES it with Node's default roots (never replaces them,
16+
* so non-intercepted endpoints still validate) and is honored by every TLS
17+
* connection (axios default agent AND the HttpsProxyAgent tunnel). We also export
18+
* NODE_EXTRA_CA_CERTS so any child Node processes trust the same cert.
19+
*
20+
* Never throws — a misconfigured cert must not break the customer's run.
21+
*/
22+
23+
const tls = require('tls');
24+
const fs = require('fs');
25+
const logger = require('./logger').winstonLogger;
26+
27+
let _patched = false;
28+
29+
function _log(level, msg) {
30+
try { if (logger && typeof logger[level] === 'function') { logger[level](msg); } } catch (e) { /* ignore */ }
31+
}
32+
33+
function resolveCaCertPath(bsConfig) {
34+
let p = process.env.BROWSERSTACK_EXTRA_CA_CERTS;
35+
const cs = (bsConfig && bsConfig.connection_settings) || {};
36+
if ((!p || !p.trim()) && cs.proxyCaCertificate) { p = cs.proxyCaCertificate; }
37+
if ((!p || !p.trim()) && bsConfig && bsConfig.proxyCaCertificate) { p = bsConfig.proxyCaCertificate; }
38+
if (!p || !String(p).trim()) { return null; }
39+
p = String(p).trim();
40+
try {
41+
if (fs.existsSync(p) && fs.statSync(p).isFile()) { return p; }
42+
_log('warn', `proxyCaCertificate: path does not exist or is not a file, falling back to system trust store: ${p}`);
43+
} catch (e) {
44+
_log('warn', `proxyCaCertificate: failed to stat cert path ${p}: ${e.message}`);
45+
}
46+
return null;
47+
}
48+
49+
function setupCaCertificate(bsConfig) {
50+
if (_patched) { return; }
51+
try {
52+
const certPath = resolveCaCertPath(bsConfig);
53+
if (!certPath) { return; }
54+
const caPem = fs.readFileSync(certPath, 'utf8');
55+
56+
const originalCreateSecureContext = tls.createSecureContext;
57+
tls.createSecureContext = function (options = {}) {
58+
const context = originalCreateSecureContext(options);
59+
try { context.context.addCACert(caPem); } catch (e) { /* best-effort merge */ }
60+
return context;
61+
};
62+
63+
if (!process.env.NODE_EXTRA_CA_CERTS) {
64+
process.env.NODE_EXTRA_CA_CERTS = certPath;
65+
}
66+
67+
_patched = true;
68+
_log('info', `proxyCaCertificate: trusting custom CA from ${certPath} (merged with system roots).`);
69+
} catch (e) {
70+
_log('warn', `proxyCaCertificate: setup failed, falling back to system trust store: ${e.message}`);
71+
}
72+
}
73+
74+
module.exports = { resolveCaCertPath, setupCaCertificate };

bin/helpers/utils.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ exports.validateBstackJson = (bsConfigPath) => {
3535
logger.info(`Reading config from ${bsConfigPath}`);
3636
let bsConfig = require(bsConfigPath);
3737
bsConfig = exports.normalizeTestReportingConfig(bsConfig);
38+
// SDK-5953: trust the customer CA (proxyCaCertificate / BROWSERSTACK_EXTRA_CA_CERTS)
39+
// for all outbound HTTPS (axios) before any request fires. Merged with system roots.
40+
try { require('./caCertHelper').setupCaCertificate(bsConfig); } catch (e) { /* never break the run */ }
3841
resolve(bsConfig);
3942
} catch (e) {
4043
reject(

0 commit comments

Comments
 (0)