11import * as _ from 'lodash' ;
22import { spawn , execFile , ChildProcess } from 'child_process' ;
3+ import * as https from 'https' ;
34import * as path from 'path' ;
45
56import { 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+
6077describe ( '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