Skip to content

Commit

Permalink
Add option tlsIntercept
Browse files Browse the repository at this point in the history
  • Loading branch information
lipsumar committed Mar 28, 2024
1 parent ad59f34 commit 93a909a
Show file tree
Hide file tree
Showing 3 changed files with 84 additions and 3 deletions.
12 changes: 12 additions & 0 deletions src/mockttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,18 @@ export type MockttpHttpsOptions = CAOptions & {
* here for additional configuration of this behaviour.
*/
tlsPassthrough?: Array<{ hostname: string }>

/**
* A list of hostnames that should only be intercepted.
*
* When set, only connections to these hostnames will be intercepted, and all
* other connections will be passed through without interception.
*
* Each element in the list must be an object with a 'hostname' field for the
* hostname that should be matched. In future more options may be supported
* here for additional configuration of this behaviour.
*/
tlsIntercept?: Array<{ hostname: string }>
};

export interface MockttpOptions {
Expand Down
21 changes: 18 additions & 3 deletions src/server/http-combo-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ export async function createComboServer(
analyzeAndMaybePassThroughTls(
tlsServer,
options.https.tlsPassthrough ?? [],
options.https.tlsIntercept ?? [],
tlsPassthroughListener
);

Expand Down Expand Up @@ -369,9 +370,14 @@ function copyTimingDetails<T extends SocketIsh<'__timingInfo'>>(
function analyzeAndMaybePassThroughTls(
server: tls.Server,
passthroughList: Required<MockttpHttpsOptions>['tlsPassthrough'],
interceptList: Required<MockttpHttpsOptions>['tlsIntercept'],
passthroughListener: (socket: net.Socket, address: string, port?: number) => void
) {
const hostnames = passthroughList.map(({ hostname }) => hostname);
if (passthroughList.length > 0 && interceptList.length > 0){
throw new Error('Cannot use both tlsPassthrough and tlsIntercept at the same time.');
}
const passThroughHostnames = passthroughList.map(({ hostname }) => hostname);
const interceptHostnames = interceptList.map(({ hostname }) => hostname);

const tlsConnectionListener = server.listeners('connection')[0] as (socket: net.Socket) => {};
server.removeListener('connection', tlsConnectionListener);
Expand All @@ -389,12 +395,21 @@ function analyzeAndMaybePassThroughTls(
clientAlpn: helloData.alpnProtocols,
ja3Fingerprint: calculateJa3FromFingerprintData(helloData.fingerprintData)
};

if (interceptHostnames.length > 0 && connectHostname && !interceptHostnames.includes(connectHostname)) {
const upstreamPort = connectPort ? parseInt(connectPort, 10) : undefined;
passthroughListener(socket, connectHostname, upstreamPort);
return; // Do not continue with TLS
} else if (interceptHostnames.length > 0 && sniHostname && !interceptHostnames.includes(sniHostname)) {
passthroughListener(socket, sniHostname); // Can't guess the port - not included in SNI
return; // Do not continue with TLS
}

if (connectHostname && hostnames.includes(connectHostname)) {
if (connectHostname && passThroughHostnames.includes(connectHostname)) {
const upstreamPort = connectPort ? parseInt(connectPort, 10) : undefined;
passthroughListener(socket, connectHostname, upstreamPort);
return; // Do not continue with TLS
} else if (sniHostname && hostnames.includes(sniHostname)) {
} else if (sniHostname && passThroughHostnames.includes(sniHostname)) {
passthroughListener(socket, sniHostname); // Can't guess the port - not included in SNI
return; // Do not continue with TLS
}
Expand Down
54 changes: 54 additions & 0 deletions test/integration/https.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,5 +252,59 @@ describe("When configured for HTTPS", () => {
});
});

describe("with some hostnames included", () => {
let server = getLocal({
https: {
keyPath: './test/fixtures/test-ca.key',
certPath: './test/fixtures/test-ca.pem',
tlsIntercept: [
{ hostname: 'wikipedia.org' }
]
}
});

beforeEach(async () => {
await server.start();
await server.forGet('/').thenReply(200, "Mock response");
});

afterEach(async () => {
await server.stop()
});

it("handles matching HTTPS requests", async () => {
const response: http.IncomingMessage = await new Promise((resolve) =>
https.get({
host: 'localhost',
port: server.port,
servername: 'wikipedia.org',
headers: { 'Host': 'wikipedia.org' }
}).on('response', resolve)
);

expect(response.statusCode).to.equal(200);
const body = (await streamToBuffer(response)).toString();
expect(body).to.equal("Mock response");
});

it("skips the server for non-matching HTTPS requests", async function () {
this.retries(3); // Example.com can be unreliable

const response: http.IncomingMessage = await new Promise((resolve, reject) =>
https.get({
host: 'localhost',
port: server.port,
servername: 'example.com',
headers: { 'Host': 'example.com' }
}).on('response', resolve).on('error', reject)
);

expect(response.statusCode).to.equal(200);
const body = (await streamToBuffer(response)).toString();
expect(body).to.include(
"This domain is for use in illustrative examples in documents."
);
});
});
});
});

0 comments on commit 93a909a

Please sign in to comment.