Skip to content

Commit 9b5c1f6

Browse files
committed
Allow disabling certificate checks for specific hosts when passing through requests
1 parent a11b96d commit 9b5c1f6

File tree

4 files changed

+77
-24
lines changed

4 files changed

+77
-24
lines changed

src/rules/handlers.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -381,11 +381,22 @@ export class StreamHandlerData extends Serializable {
381381
}
382382
}
383383

384+
export interface PassThroughHandlerOptions {
385+
forwardToLocation?: string;
386+
ignoreHostCertificateErrors?: string[];
387+
}
388+
384389
export class PassThroughHandlerData extends Serializable {
385390
readonly type: 'passthrough' = 'passthrough';
386391

387-
constructor(private forwardToLocation?: string) {
392+
private forwardToLocation?: string;
393+
private ignoreHostCertificateErrors: string[] = [];
394+
395+
constructor(options: PassThroughHandlerOptions = {}) {
388396
super();
397+
398+
this.forwardToLocation = options.forwardToLocation;
399+
this.ignoreHostCertificateErrors = options.ignoreHostCertificateErrors || [];
389400
}
390401

391402
buildHandler() {
@@ -419,6 +430,8 @@ export class PassThroughHandlerData extends Serializable {
419430
protocol = clientReq.protocol + ':';
420431
}
421432

433+
const checkServerCertificate = !_.includes(this.ignoreHostCertificateErrors, hostname);
434+
422435
let makeRequest = protocol === 'https:' ? https.request : http.request;
423436

424437
let outgoingPort: null | number = null;
@@ -429,7 +442,8 @@ export class PassThroughHandlerData extends Serializable {
429442
hostname,
430443
port,
431444
path,
432-
headers
445+
headers,
446+
rejectUnauthorized: checkServerCertificate
433447
}, (serverRes) => {
434448
Object.keys(serverRes.headers).forEach((header) => {
435449
try {

src/rules/mock-rule-builder.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import url = require("url");
66
import { OutgoingHttpHeaders } from "http";
7-
import { merge } from "lodash";
7+
import { merge, defaults } from "lodash";
88
import { Readable } from "stream";
99
import { stripIndent } from "common-tags";
1010

@@ -43,7 +43,8 @@ import {
4343
CallbackHandlerResult,
4444
StreamHandlerData,
4545
CloseConnectionHandlerData,
46-
TimeoutHandlerData
46+
TimeoutHandlerData,
47+
PassThroughHandlerOptions
4748
} from "./handlers";
4849

4950
/**
@@ -303,6 +304,11 @@ export default class MockRuleBuilder {
303304
* for proxied requests only, direct requests will be rejected with
304305
* an error.
305306
*
307+
* This method takes options to configure how the request is passed
308+
* through. The only option currently supported is ignoreHostCertificateErrors,
309+
* a list of hostnames for which server certificate errors should
310+
* be ignored (none, by default).
311+
*
306312
* Calling this method registers the rule with the server, so it
307313
* starts to handle requests.
308314
*
@@ -311,11 +317,11 @@ export default class MockRuleBuilder {
311317
* before sending requests to be matched. The mocked endpoint
312318
* can be used to assert on the requests matched by this rule.
313319
*/
314-
thenPassThrough(): Promise<MockedEndpoint> {
320+
thenPassThrough(options?: PassThroughHandlerOptions): Promise<MockedEndpoint> {
315321
const rule: MockRuleData = {
316322
matchers: this.matchers,
317323
completionChecker: this.isComplete,
318-
handler: new PassThroughHandlerData()
324+
handler: new PassThroughHandlerData(options)
319325
};
320326

321327
return this.addRule(rule);
@@ -326,6 +332,11 @@ export default class MockRuleBuilder {
326332
* specified must not include a path. Otherwise, an error is thrown.
327333
* The path portion of the original request url is used instead.
328334
*
335+
* This method also takes options to configure how the request is passed
336+
* through. The only option currently supported is ignoreHostCertificateErrors,
337+
* a list of hostnames for which server certificate errors should
338+
* be ignored (none, by default).
339+
*
329340
* Calling this method registers the rule with the server, so it
330341
* starts to handle requests.
331342
*
@@ -334,20 +345,20 @@ export default class MockRuleBuilder {
334345
* before sending requests to be matched. The mocked endpoint
335346
* can be used to assert on the requests matched by this rule.
336347
*/
337-
async thenForwardTo(forwardToUrl: string): Promise<MockedEndpoint> {
338-
const { protocol, hostname, port, path } = url.parse(forwardToUrl);
348+
async thenForwardTo(forwardToLocation: string, options?: PassThroughHandlerOptions): Promise<MockedEndpoint> {
349+
const { protocol, hostname, port, path } = url.parse(forwardToLocation);
339350
if (path && path.trim() !== "/") {
340351
const suggestion = url.format({ protocol, hostname, port });
341352
throw new Error(stripIndent`
342-
URLs passed to thenForwardTo cannot include a path, but "${forwardToUrl}" does. ${''
353+
URLs passed to thenForwardTo cannot include a path, but "${forwardToLocation}" does. ${''
343354
}Did you mean ${suggestion}?
344355
`);
345356
}
346357

347358
const rule: MockRuleData = {
348359
matchers: this.matchers,
349360
completionChecker: this.isComplete,
350-
handler: new PassThroughHandlerData(forwardToUrl)
361+
handler: new PassThroughHandlerData(defaults({ forwardToLocation }, options))
351362
};
352363

353364
return this.addRule(rule);

src/util/tls.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,13 @@ export type GeneratedCertificate = {
4141
* These can be saved to disk, and their paths passed
4242
* as HTTPS options to a Mockttp server.
4343
*/
44-
export function generateCACertificate(options: { commonName: string } = {
45-
commonName: 'Mockttp Testing CA - DO NOT TRUST - TESTING ONLY',
46-
}) {
47-
const keyPair = pki.rsa.generateKeyPair(2048);
44+
export function generateCACertificate(options: { commonName?: string, bytes?: number } = {}) {
45+
options = _.defaults({}, options, {
46+
commonName: 'Mockttp Testing CA - DO NOT TRUST - TESTING ONLY',
47+
bytes: 2048
48+
});
49+
50+
const keyPair = pki.rsa.generateKeyPair(options.bytes);
4851
const cert = pki.createCertificate();
4952
cert.publicKey = keyPair.publicKey;
5053
cert.serialNumber = uuid().replace(/-/g, '');

test/integration/proxy.spec.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import _ = require("lodash");
22
import { getLocal, Mockttp } from "../..";
33
import request = require("request-promise-native");
44
import { expect, nodeOnly } from "../test-utils";
5-
import { getCA } from "../../src/util/tls";
5+
import { getCA, generateCACertificate } from "../../src/util/tls";
66

77
const INITIAL_ENV = _.cloneDeep(process.env);
88

@@ -143,15 +143,10 @@ nodeOnly(() => {
143143
describe("given an untrusted upstream certificate", () => {
144144

145145
let badServer: Mockttp;
146+
const untrustedCACert = generateCACertificate({ bytes: 512 });
146147

147148
beforeEach(async () => {
148-
const ca = await getCA({
149-
keyPath: './test/fixtures/test-ca.key',
150-
certPath: './test/fixtures/test-ca.pem'
151-
});
152-
const certificate = ca.generateCertificate('wrong-domain');
153-
154-
badServer = getLocal({ https: certificate });
149+
badServer = getLocal({ https: untrustedCACert });
155150
await badServer.start();
156151
});
157152

@@ -160,9 +155,39 @@ nodeOnly(() => {
160155
it("should refuse to pass through requests", async () => {
161156
await badServer.anyRequest().thenReply(200);
162157

163-
await server.get(badServer.urlFor('/')).thenPassThrough();
158+
await server.anyRequest().thenPassThrough();
159+
160+
let response = await request.get(badServer.url, {
161+
resolveWithFullResponse: true,
162+
simple: false
163+
});
164+
165+
expect(response.statusCode).to.equal(502);
166+
});
167+
168+
it("should allow passing through requests if the host is specifically listed", async () => {
169+
await badServer.anyRequest().thenReply(200);
170+
171+
await server.anyRequest().thenPassThrough({
172+
ignoreHostCertificateErrors: ['localhost']
173+
});
174+
175+
let response = await request.get(badServer.url, {
176+
resolveWithFullResponse: true,
177+
simple: false
178+
});
179+
180+
expect(response.statusCode).to.equal(200);
181+
});
182+
183+
it("should allow passing through requests if a non-matching host is specifically listed", async () => {
184+
await badServer.anyRequest().thenReply(200);
185+
186+
await server.get(badServer.urlFor('/')).thenPassThrough({
187+
ignoreHostCertificateErrors: ['differenthost']
188+
});
164189

165-
let response = await request.get(badServer.urlFor('/'), {
190+
let response = await request.get(badServer.url, {
166191
resolveWithFullResponse: true,
167192
simple: false
168193
});

0 commit comments

Comments
 (0)