Skip to content

Commit 93a909a

Browse files
committed
Add option tlsIntercept
1 parent ad59f34 commit 93a909a

File tree

3 files changed

+84
-3
lines changed

3 files changed

+84
-3
lines changed

src/mockttp.ts

+12
Original file line numberDiff line numberDiff line change
@@ -696,6 +696,18 @@ export type MockttpHttpsOptions = CAOptions & {
696696
* here for additional configuration of this behaviour.
697697
*/
698698
tlsPassthrough?: Array<{ hostname: string }>
699+
700+
/**
701+
* A list of hostnames that should only be intercepted.
702+
*
703+
* When set, only connections to these hostnames will be intercepted, and all
704+
* other connections will be passed through without interception.
705+
*
706+
* Each element in the list must be an object with a 'hostname' field for the
707+
* hostname that should be matched. In future more options may be supported
708+
* here for additional configuration of this behaviour.
709+
*/
710+
tlsIntercept?: Array<{ hostname: string }>
699711
};
700712

701713
export interface MockttpOptions {

src/server/http-combo-server.ts

+18-3
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ export async function createComboServer(
201201
analyzeAndMaybePassThroughTls(
202202
tlsServer,
203203
options.https.tlsPassthrough ?? [],
204+
options.https.tlsIntercept ?? [],
204205
tlsPassthroughListener
205206
);
206207

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

376382
const tlsConnectionListener = server.listeners('connection')[0] as (socket: net.Socket) => {};
377383
server.removeListener('connection', tlsConnectionListener);
@@ -389,12 +395,21 @@ function analyzeAndMaybePassThroughTls(
389395
clientAlpn: helloData.alpnProtocols,
390396
ja3Fingerprint: calculateJa3FromFingerprintData(helloData.fingerprintData)
391397
};
398+
399+
if (interceptHostnames.length > 0 && connectHostname && !interceptHostnames.includes(connectHostname)) {
400+
const upstreamPort = connectPort ? parseInt(connectPort, 10) : undefined;
401+
passthroughListener(socket, connectHostname, upstreamPort);
402+
return; // Do not continue with TLS
403+
} else if (interceptHostnames.length > 0 && sniHostname && !interceptHostnames.includes(sniHostname)) {
404+
passthroughListener(socket, sniHostname); // Can't guess the port - not included in SNI
405+
return; // Do not continue with TLS
406+
}
392407

393-
if (connectHostname && hostnames.includes(connectHostname)) {
408+
if (connectHostname && passThroughHostnames.includes(connectHostname)) {
394409
const upstreamPort = connectPort ? parseInt(connectPort, 10) : undefined;
395410
passthroughListener(socket, connectHostname, upstreamPort);
396411
return; // Do not continue with TLS
397-
} else if (sniHostname && hostnames.includes(sniHostname)) {
412+
} else if (sniHostname && passThroughHostnames.includes(sniHostname)) {
398413
passthroughListener(socket, sniHostname); // Can't guess the port - not included in SNI
399414
return; // Do not continue with TLS
400415
}

test/integration/https.spec.ts

+54
Original file line numberDiff line numberDiff line change
@@ -252,5 +252,59 @@ describe("When configured for HTTPS", () => {
252252
});
253253
});
254254

255+
describe("with some hostnames included", () => {
256+
let server = getLocal({
257+
https: {
258+
keyPath: './test/fixtures/test-ca.key',
259+
certPath: './test/fixtures/test-ca.pem',
260+
tlsIntercept: [
261+
{ hostname: 'wikipedia.org' }
262+
]
263+
}
264+
});
265+
266+
beforeEach(async () => {
267+
await server.start();
268+
await server.forGet('/').thenReply(200, "Mock response");
269+
});
270+
271+
afterEach(async () => {
272+
await server.stop()
273+
});
274+
275+
it("handles matching HTTPS requests", async () => {
276+
const response: http.IncomingMessage = await new Promise((resolve) =>
277+
https.get({
278+
host: 'localhost',
279+
port: server.port,
280+
servername: 'wikipedia.org',
281+
headers: { 'Host': 'wikipedia.org' }
282+
}).on('response', resolve)
283+
);
284+
285+
expect(response.statusCode).to.equal(200);
286+
const body = (await streamToBuffer(response)).toString();
287+
expect(body).to.equal("Mock response");
288+
});
289+
290+
it("skips the server for non-matching HTTPS requests", async function () {
291+
this.retries(3); // Example.com can be unreliable
292+
293+
const response: http.IncomingMessage = await new Promise((resolve, reject) =>
294+
https.get({
295+
host: 'localhost',
296+
port: server.port,
297+
servername: 'example.com',
298+
headers: { 'Host': 'example.com' }
299+
}).on('response', resolve).on('error', reject)
300+
);
301+
302+
expect(response.statusCode).to.equal(200);
303+
const body = (await streamToBuffer(response)).toString();
304+
expect(body).to.include(
305+
"This domain is for use in illustrative examples in documents."
306+
);
307+
});
308+
});
255309
});
256310
});

0 commit comments

Comments
 (0)