Skip to content

Commit

Permalink
Allow disabling certificate checks for specific hosts when passing th…
Browse files Browse the repository at this point in the history
…rough requests
  • Loading branch information
pimterry committed Jan 16, 2019
1 parent a11b96d commit 8717dad
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 24 deletions.
18 changes: 16 additions & 2 deletions src/rules/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,11 +381,22 @@ export class StreamHandlerData extends Serializable {
}
}

export interface PassThroughHandlerOptions {
forwardToLocation?: string;
ignoreHostCertificateErrors?: string[];
}

export class PassThroughHandlerData extends Serializable {
readonly type: 'passthrough' = 'passthrough';

constructor(private forwardToLocation?: string) {
private forwardToLocation?: string;
private ignoreHostCertificateErrors: string[] = [];

constructor(options: PassThroughHandlerOptions = {}) {
super();

this.forwardToLocation = options.forwardToLocation;
this.ignoreHostCertificateErrors = options.ignoreHostCertificateErrors || [];
}

buildHandler() {
Expand Down Expand Up @@ -419,6 +430,8 @@ export class PassThroughHandlerData extends Serializable {
protocol = clientReq.protocol + ':';
}

const checkServerCertificate = !_.includes(this.ignoreHostCertificateErrors, hostname);

let makeRequest = protocol === 'https:' ? https.request : http.request;

let outgoingPort: null | number = null;
Expand All @@ -429,7 +442,8 @@ export class PassThroughHandlerData extends Serializable {
hostname,
port,
path,
headers
headers,
rejectUnauthorized: checkServerCertificate
}, (serverRes) => {
Object.keys(serverRes.headers).forEach((header) => {
try {
Expand Down
27 changes: 19 additions & 8 deletions src/rules/mock-rule-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import url = require("url");
import { OutgoingHttpHeaders } from "http";
import { merge } from "lodash";
import { merge, defaults } from "lodash";
import { Readable } from "stream";
import { stripIndent } from "common-tags";

Expand Down Expand Up @@ -43,7 +43,8 @@ import {
CallbackHandlerResult,
StreamHandlerData,
CloseConnectionHandlerData,
TimeoutHandlerData
TimeoutHandlerData,
PassThroughHandlerOptions
} from "./handlers";

/**
Expand Down Expand Up @@ -303,6 +304,11 @@ export default class MockRuleBuilder {
* for proxied requests only, direct requests will be rejected with
* an error.
*
* This method takes options to configure how the request is passed
* through. The only option currently supported is ignoreHostCertificateErrors,
* a list of hostnames for which server certificate errors should
* be ignored (none, by default).
*
* Calling this method registers the rule with the server, so it
* starts to handle requests.
*
Expand All @@ -311,11 +317,11 @@ export default class MockRuleBuilder {
* before sending requests to be matched. The mocked endpoint
* can be used to assert on the requests matched by this rule.
*/
thenPassThrough(): Promise<MockedEndpoint> {
thenPassThrough(options?: PassThroughHandlerOptions): Promise<MockedEndpoint> {
const rule: MockRuleData = {
matchers: this.matchers,
completionChecker: this.isComplete,
handler: new PassThroughHandlerData()
handler: new PassThroughHandlerData(options)
};

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

const rule: MockRuleData = {
matchers: this.matchers,
completionChecker: this.isComplete,
handler: new PassThroughHandlerData(forwardToUrl)
handler: new PassThroughHandlerData(defaults({ forwardToLocation }, options))
};

return this.addRule(rule);
Expand Down
11 changes: 7 additions & 4 deletions src/util/tls.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,13 @@ export type GeneratedCertificate = {
* These can be saved to disk, and their paths passed
* as HTTPS options to a Mockttp server.
*/
export function generateCACertificate(options: { commonName: string } = {
commonName: 'Mockttp Testing CA - DO NOT TRUST - TESTING ONLY',
}) {
const keyPair = pki.rsa.generateKeyPair(2048);
export function generateCACertificate(options: { commonName?: string, bytes?: number } = {}) {
options = _.defaults({}, options, {
commonName: 'Mockttp Testing CA - DO NOT TRUST - TESTING ONLY',
bytes: 2048
});

const keyPair = pki.rsa.generateKeyPair(options.bytes);
const cert = pki.createCertificate();
cert.publicKey = keyPair.publicKey;
cert.serialNumber = uuid().replace(/-/g, '');
Expand Down
45 changes: 35 additions & 10 deletions test/integration/proxy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import _ = require("lodash");
import { getLocal, Mockttp } from "../..";
import request = require("request-promise-native");
import { expect, nodeOnly } from "../test-utils";
import { getCA } from "../../src/util/tls";
import { getCA, generateCACertificate } from "../../src/util/tls";

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

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

let badServer: Mockttp;
const untrustedCACert = generateCACertificate({ bytes: 1024 });

beforeEach(async () => {
const ca = await getCA({
keyPath: './test/fixtures/test-ca.key',
certPath: './test/fixtures/test-ca.pem'
});
const certificate = ca.generateCertificate('wrong-domain');

badServer = getLocal({ https: certificate });
badServer = getLocal({ https: untrustedCACert });
await badServer.start();
});

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

await server.get(badServer.urlFor('/')).thenPassThrough();
await server.anyRequest().thenPassThrough();

let response = await request.get(badServer.url, {
resolveWithFullResponse: true,
simple: false
});

expect(response.statusCode).to.equal(502);
});

it("should allow passing through requests if the host is specifically listed", async () => {
await badServer.anyRequest().thenReply(200);

await server.anyRequest().thenPassThrough({
ignoreHostCertificateErrors: ['localhost']
});

let response = await request.get(badServer.url, {
resolveWithFullResponse: true,
simple: false
});

expect(response.statusCode).to.equal(200);
});

it("should allow passing through requests if a non-matching host is specifically listed", async () => {
await badServer.anyRequest().thenReply(200);

await server.get(badServer.urlFor('/')).thenPassThrough({
ignoreHostCertificateErrors: ['differenthost']
});

let response = await request.get(badServer.urlFor('/'), {
let response = await request.get(badServer.url, {
resolveWithFullResponse: true,
simple: false
});
Expand Down

0 comments on commit 8717dad

Please sign in to comment.