Skip to content

Commit c6ff5cf

Browse files
committed
Add tests covering TLS impersonation
1 parent 87474bf commit c6ff5cf

3 files changed

Lines changed: 70 additions & 0 deletions

File tree

‎test/distributables-test/unix.sh‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ echo "\nCan start a Mockttp server"?
3232
curl $CURL_OPTIONS "$WITH_ORIGIN" "$AS_JSON" 'http://127.0.0.1:45456/start' \
3333
--data '{"plugins":{"http":{"options":{"cors":false,"suggestChanges":false,"http2":"fallback","https":{"tlsPassthrough":[]}}},"webrtc":{}}}'
3434

35+
echo "\nIs TLS fingerprint mirroring available?"
36+
# The UI turns mirroring on for every passthrough rule, but it needs tls-impersonate: an
37+
# optional native module that the bundle requires externally, so it's easily lost when
38+
# packaging. Resolve it exactly as the bundle does, and check the native addon loads.
39+
./httptoolkit-server/bin/node -e "if (!require(require.resolve('tls-impersonate', { paths: ['./httptoolkit-server/bundle'] })).isSupported()) { console.error('TLS fingerprint mirroring is unavailable in this build'); process.exit(1); }"
40+
3541
echo "\nCan query the API server version?"
3642
curl $CURL_OPTIONS "$WITH_ORIGIN" http://127.0.0.1:45457/version
3743

‎test/distributables-test/windows.bat‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ curl %CURL_OPTIONS% %WITH_ORIGIN% %AS_JSON% -X POST "http://127.0.0.1:45456/star
4545
--data "{\"plugins\":{\"http\":{\"options\":{\"cors\":false,\"suggestChanges\":false,\"http2\":\"fallback\",\"https\":{\"tlsPassthrough\":[]}}},\"webrtc\":{}}}" ^
4646
|| goto :error`
4747

48+
echo:
49+
echo:
50+
echo Is TLS fingerprint mirroring available?
51+
REM The UI turns mirroring on for every passthrough rule, but it needs tls-impersonate: an
52+
REM optional native module that the bundle requires externally, so it's easily lost when
53+
REM packaging. Resolve it exactly as the bundle does, and check the native addon loads.
54+
.\httptoolkit-server\bin\node.exe -e "if (!require(require.resolve('tls-impersonate', { paths: ['./httptoolkit-server/bundle'] })).isSupported()) { console.error('TLS fingerprint mirroring is unavailable in this build'); process.exit(1); }" || goto :error
55+
4856
echo:
4957
echo:
5058
echo Can query the API server version?

‎test/integration/e2e-api-test.spec.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as _ from 'lodash';
22
import { spawn, execFile, ChildProcess } from 'child_process';
3+
import * as https from 'https';
34
import * as path from 'path';
45

56
import { delay } from '@httptoolkit/util';
@@ -57,6 +58,22 @@ const hasExited = (process: ChildProcess) => {
5758
process.signalCode !== null;
5859
};
5960

61+
// Reports the TLS fingerprint of the client hello it received:
62+
const FINGERPRINT_HOST = 'https://testserver.host';
63+
const FINGERPRINT_PATH = '/tls/fingerprint';
64+
65+
// Check the fingerprint when sending a custom cipher list (should be custom fingerprint)
66+
const getSeenFingerprint = async (url: string) => {
67+
const response = await fetch(url, {
68+
agent: new https.Agent({
69+
rejectUnauthorized: false, // Mockttp intercepts this with its own CA
70+
ciphers: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:AES256-SHA'
71+
})
72+
});
73+
const { ja4 } = await response.json() as { ja4: string };
74+
return ja4;
75+
};
76+
6077
describe('End-to-end server API test', function () {
6178
// Timeout needs to be long, as first test runs (e.g. in CI) generate
6279
// fresh certificates, which can take a little while.
@@ -490,4 +507,43 @@ describe('End-to-end server API test', function () {
490507
expect(_.omit(responseEvents[3], 'timestamp')).to.deep.equal({ type: 'response-end' });
491508
});
492509
})
510+
511+
// This verifies that tls-impersonate (bundled native module) is working correctly:
512+
describe('mirroring TLS fingerprints upstream', () => {
513+
514+
const mockttp = getRemote({
515+
adminServerUrl: 'http://localhost:45456',
516+
client: {
517+
// Pretend to be a browser on the real site:
518+
headers: { origin: 'https://app.httptoolkit.tech' }
519+
}
520+
});
521+
522+
beforeEach(() => mockttp.start());
523+
afterEach(() => mockttp.stop());
524+
525+
const getUpstreamFingerprint = async (mirrorTlsFingerprint: boolean) => {
526+
await mockttp.forAnyRequest().thenForwardTo(FINGERPRINT_HOST, { mirrorTlsFingerprint });
527+
return getSeenFingerprint(
528+
mockttp.url.replace('http:', 'https:') + FINGERPRINT_PATH
529+
);
530+
};
531+
532+
it('reproduces our fingerprint upstream', async () => {
533+
expect(await getUpstreamFingerprint(true)).to.equal(
534+
await getSeenFingerprint(FINGERPRINT_HOST + FINGERPRINT_PATH),
535+
"Upstream JA4 should match our own - if it doesn't, the tls-impersonate " +
536+
'native module is probably missing from this build'
537+
);
538+
});
539+
540+
it('sends its own fingerprint when mirroring is off', async () => {
541+
// Confirms the test above can't pass for trivial reasons:
542+
expect(await getUpstreamFingerprint(false)).not.to.equal(
543+
await getSeenFingerprint(FINGERPRINT_HOST + FINGERPRINT_PATH)
544+
);
545+
});
546+
547+
});
548+
493549
});

0 commit comments

Comments
 (0)