Skip to content

Commit 6f329a1

Browse files
committed
Add support for 'additionalTrustedCAs' option in passthrough proxy config
1 parent ec6a82c commit 6f329a1

File tree

8 files changed

+138
-32
lines changed

8 files changed

+138
-32
lines changed

src/main.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export type {
5757
ProxySettingCallbackParams
5858
} from './rules/proxy-config';
5959
export type {
60+
CADefinition,
6061
ForwardingOptions,
6162
PassThroughLookupOptions,
6263
PassThroughHandlerConnectionOptions

src/rules/http-agents.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const getSocksProxyAgent = (opts: any) => new SocksProxyAgent(opts);
1212

1313
import { isNode } from "../util/util";
1414
import { getProxySetting, matchesNoProxy, ProxySettingSource } from './proxy-config';
15+
import { getTrustedCAs } from './passthrough-handling';
1516

1617
const KeepAliveAgents = isNode
1718
? { // These are only used (and only available) on the node server side
@@ -70,24 +71,30 @@ export async function getAgent({
7071

7172
const cacheKey = getCacheKey({
7273
url: proxySetting.proxyUrl,
73-
ca: proxySetting.trustedCAs
74+
trustedCAs: proxySetting.trustedCAs,
75+
additionalTrustedCAs: proxySetting.additionalTrustedCAs
7476
});
7577

7678
if (!proxyAgentCache.has(cacheKey)) {
7779
const { protocol, auth, hostname, port } = url.parse(proxySetting.proxyUrl);
7880
const buildProxyAgent = ProxyAgentFactoryMap[protocol as keyof typeof ProxyAgentFactoryMap];
7981

82+
// If you specify trusted CAs, we override the CAs used for this connection, i.e. the trusted
83+
// CA for the certificate of an HTTPS proxy. This is *not* the CAs trusted for upstream servers
84+
// on the otherside of the proxy - see the corresponding passthrough options for that.
85+
const trustedCerts = await getTrustedCAs(
86+
proxySetting.trustedCAs,
87+
proxySetting.additionalTrustedCAs
88+
);
89+
8090
proxyAgentCache.set(cacheKey, buildProxyAgent({
8191
protocol,
8292
auth,
8393
hostname,
8494
port,
8595

86-
// If you specify trusted CAs, we override the CAs used for this connection, i.e. the trusted
87-
// CA for the certificate of an HTTPS proxy. This is *not* the CAs trusted for upstream servers
88-
// on the otherside of the proxy - see the `trustAdditionalCAs` passthrough option for that.
89-
...(proxySetting.trustedCAs
90-
? { ca: proxySetting.trustedCAs }
96+
...(trustedCerts
97+
? { ca: trustedCerts }
9198
: {}
9299
)
93100
}));

src/rules/passthrough-handling-definitions.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export interface PassThroughLookupOptions {
2828
servers?: string[];
2929
}
3030

31+
export type CADefinition =
32+
| { cert: string | Buffer }
33+
| { certPath: string };
34+
3135
/**
3236
* This defines the upstream connection parameters. These passthrough parameters
3337
* are shared between both WebSocket & Request passthrough rules.
@@ -62,7 +66,7 @@ export interface PassThroughHandlerConnectionOptions {
6266
* or buffer value containing the PEM certificate, or a `certPath` key and a
6367
* string value containing the local path to the PEM certificate.
6468
*/
65-
trustAdditionalCAs?: Array<{ cert: string | Buffer } | { certPath: string }>;
69+
trustAdditionalCAs?: Array<CADefinition>;
6670

6771
/**
6872
* A mapping of hosts to client certificates to use, in the form of

src/rules/passthrough-handling.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as _ from 'lodash';
2+
import * as fs from 'fs/promises';
23
import * as tls from 'tls';
34
import url = require('url');
45
import { oneLine } from 'common-tags';
@@ -17,6 +18,7 @@ import {
1718
CallbackResponseMessageResult
1819
} from './requests/request-handler-definitions';
1920
import {
21+
CADefinition,
2022
PassThroughLookupOptions
2123
} from './passthrough-handling-definitions';
2224

@@ -97,6 +99,34 @@ export const getUpstreamTlsOptions = (strictChecks: boolean): tls.SecureContextO
9799
rejectUnauthorized: strictChecks,
98100
});
99101

102+
export async function getTrustedCAs(
103+
trustedCAs: Array<string | CADefinition> | undefined,
104+
additionalTrustedCAs: Array<CADefinition> | undefined
105+
): Promise<Array<string> | undefined> {
106+
if (trustedCAs && additionalTrustedCAs) {
107+
throw new Error(`trustedCAs and additionalTrustedCAs options are mutually exclusive`);
108+
}
109+
110+
if (trustedCAs) {
111+
return Promise.all(trustedCAs.map((caDefinition) => getCA(caDefinition)));
112+
}
113+
114+
if (additionalTrustedCAs) {
115+
const CAs = await Promise.all(additionalTrustedCAs.map((caDefinition) => getCA(caDefinition)));
116+
return tls.rootCertificates.concat(CAs);
117+
}
118+
}
119+
120+
const getCA = async (caDefinition: string | CADefinition) => {
121+
return typeof caDefinition === 'string'
122+
? caDefinition
123+
: 'certPath' in caDefinition
124+
? await fs.readFile(caDefinition.certPath, 'utf8')
125+
// 'cert' in caDefinition
126+
: caDefinition.cert.toString('utf8')
127+
}
128+
129+
100130
// --- Various helpers for deriving parts of request/response data given partial overrides: ---
101131

102132
/**

src/rules/proxy-config.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import * as _ from 'lodash';
22

33
import { MaybePromise } from '../util/type-utils';
44
import { RuleParameterReference } from './rule-parameters';
5+
import { CADefinition } from './passthrough-handling-definitions';
56

67
/**
78
* A ProxySetting is a specific proxy setting to use, which is passed to a proxy agent
@@ -41,10 +42,27 @@ export interface ProxySetting {
4142
* the proxy is not HTTPS. If not specified, this will default to the Node
4243
* defaults, or you can override them here completely.
4344
*
44-
* Note that unlike passthrough rule's `trustAdditionalCAs` option, this sets the
45-
* complete list of trusted CAs - not just additional ones.
45+
* This sets the complete list of trusted CAs, and is mutually exclusive with the
46+
* `additionalTrustedCAs` option, which adds additional CAs (but also trusts the
47+
* Node default CAs too).
48+
*
49+
* This should be specified as either a { cert: string | Buffer } object or a
50+
* { certPath: string } object (to read the cert from disk). The previous
51+
* simple string format is supported but deprecated.
52+
*/
53+
trustedCAs?: Array<
54+
| string // Deprecated
55+
| CADefinition
56+
>;
57+
58+
/**
59+
* Extra CAs to trust for HTTPS connections to the proxy. Ignored if the connection
60+
* to the proxy is not HTTPS.
61+
*
62+
* This appends to the list of trusted CAs, and is mutually exclusive with the
63+
* `trustedCAs` option, which completely overrides the list of CAs.
4664
*/
47-
trustedCAs?: string[];
65+
additionalTrustedCAs?: Array<CADefinition>;
4866
}
4967

5068
/**

src/rules/requests/request-handlers.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ import {
7878
getUpstreamTlsOptions,
7979
shouldUseStrictHttps,
8080
getClientRelativeHostname,
81-
getDnsLookupFunction
81+
getDnsLookupFunction,
82+
getTrustedCAs
8283
} from '../passthrough-handling';
8384

8485
import {
@@ -388,16 +389,7 @@ export class PassThroughHandler extends PassThroughHandlerDefinition {
388389
if (!this.extraCACertificates.length) return undefined;
389390

390391
if (!this._trustedCACertificates) {
391-
this._trustedCACertificates = Promise.all(
392-
(tls.rootCertificates as Array<string | Promise<string>>)
393-
.concat(this.extraCACertificates.map(certObject => {
394-
if ('cert' in certObject) {
395-
return certObject.cert.toString('utf8');
396-
} else {
397-
return fs.readFile(certObject.certPath, 'utf8');
398-
}
399-
}))
400-
);
392+
this._trustedCACertificates = getTrustedCAs(undefined, this.extraCACertificates);
401393
}
402394

403395
return this._trustedCACertificates;

src/serialization/serialization.ts

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -286,13 +286,9 @@ export function deserializeBuffer(buffer: string): Buffer {
286286
const SERIALIZED_PARAM_REFERENCE = "__mockttp__param__reference__";
287287
export type SerializedRuleParameterReference<R> = { [SERIALIZED_PARAM_REFERENCE]: string };
288288

289-
export function maybeSerializeParam<T, R>(value: T | RuleParameterReference<R>): T | SerializedRuleParameterReference<R> {
290-
if (isParamReference(value)) {
291-
// Swap the symbol for a string, since we can't serialize symbols in JSON:
292-
return { [SERIALIZED_PARAM_REFERENCE]: value[MOCKTTP_PARAM_REF] };
293-
} else {
294-
return value;
295-
}
289+
function serializeParam<R>(value: RuleParameterReference<R>): SerializedRuleParameterReference<R> {
290+
// Swap the symbol for a string, since we can't serialize symbols in JSON:
291+
return { [SERIALIZED_PARAM_REFERENCE]: value[MOCKTTP_PARAM_REF] };
296292
}
297293

298294
function isSerializedRuleParam(value: any): value is SerializedRuleParameterReference<unknown> {
@@ -335,8 +331,22 @@ export function serializeProxyConfig(
335331
return callbackId;
336332
} else if (_.isArray(proxyConfig)) {
337333
return proxyConfig.map((config) => serializeProxyConfig(config, channel));
338-
} else {
339-
return maybeSerializeParam(proxyConfig);
334+
} else if (isParamReference(proxyConfig)) {
335+
return serializeParam(proxyConfig);
336+
} else if (proxyConfig) {
337+
return {
338+
...proxyConfig,
339+
trustedCAs: proxyConfig.trustedCAs?.map((caDefinition) =>
340+
typeof caDefinition !== 'string' && 'cert' in caDefinition
341+
? { cert: caDefinition.cert.toString('utf8') } // Stringify in case of buffers
342+
: caDefinition
343+
),
344+
additionalTrustedCAs: proxyConfig.additionalTrustedCAs?.map((caDefinition) =>
345+
'cert' in caDefinition
346+
? { cert: caDefinition.cert.toString('utf8') } // Stringify in case of buffers
347+
: caDefinition
348+
)
349+
}
340350
}
341351
}
342352

test/integration/proxying/upstream-proxying.spec.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ nodeOnly(() => {
287287
expect(result.message).to.match(/self(-| )signed certificate/); // Dash varies by Node version
288288
});
289289

290-
it("should trust the remote proxy's CA if explicitly specified", async () => {
290+
it("should trust the remote proxy's CA if explicitly specified by content", async () => {
291291
// Remote server sends fixed response on this one URL:
292292
await remoteServer.forGet('/test-url').thenReply(200, "Remote server says hi!");
293293

@@ -296,7 +296,51 @@ nodeOnly(() => {
296296
proxyConfig: {
297297
proxyUrl: intermediateProxy.url,
298298
trustedCAs: [
299-
(await fs.readFile('./test/fixtures/untrusted-ca.pem')).toString()
299+
{ cert: await fs.readFile('./test/fixtures/untrusted-ca.pem') }
300+
]
301+
}
302+
});
303+
304+
const response = await request.get(remoteServer.urlFor("/test-url"));
305+
306+
// We get a successful response
307+
expect(response).to.equal("Remote server says hi!");
308+
// And it went via the intermediate proxy
309+
expect((await proxyEndpoint.getSeenRequests()).length).to.equal(1);
310+
});
311+
312+
it("should trust the remote proxy's CA if explicitly specified by file path", async () => {
313+
// Remote server sends fixed response on this one URL:
314+
await remoteServer.forGet('/test-url').thenReply(200, "Remote server says hi!");
315+
316+
// Mockttp forwards requests via our intermediate proxy
317+
await server.forAnyRequest().thenPassThrough({
318+
proxyConfig: {
319+
proxyUrl: intermediateProxy.url,
320+
trustedCAs: [
321+
{ certPath: './test/fixtures/untrusted-ca.pem' }
322+
]
323+
}
324+
});
325+
326+
const response = await request.get(remoteServer.urlFor("/test-url"));
327+
328+
// We get a successful response
329+
expect(response).to.equal("Remote server says hi!");
330+
// And it went via the intermediate proxy
331+
expect((await proxyEndpoint.getSeenRequests()).length).to.equal(1);
332+
});
333+
334+
it("should trust the remote proxy's CA if explicitly specified as additional", async () => {
335+
// Remote server sends fixed response on this one URL:
336+
await remoteServer.forGet('/test-url').thenReply(200, "Remote server says hi!");
337+
338+
// Mockttp forwards requests via our intermediate proxy
339+
await server.forAnyRequest().thenPassThrough({
340+
proxyConfig: {
341+
proxyUrl: intermediateProxy.url,
342+
additionalTrustedCAs: [
343+
{ cert: (await fs.readFile('./test/fixtures/untrusted-ca.pem')).toString() }
300344
]
301345
}
302346
});

0 commit comments

Comments
 (0)