mic~ proxy for JA4-TLS fingerprinting evasion |
| git clone https://github.com/djnnvx/mic.git |
| log | files | refs | archive | README | LICENSE |
integration_test.go (4846B)
1 //go:build integration 2 3 package proxy 4 5 import ( 6 "bufio" 7 "crypto/tls" 8 "crypto/x509" 9 "encoding/pem" 10 "fmt" 11 "io" 12 "net" 13 "net/http" 14 "net/http/httptest" 15 "os" 16 "testing" 17 "time" 18 19 "github.com/djnnvx/mic/fingerprint" 20 ) 21 22 const testTimeout = 10 * time.Second 23 24 func serveHandler(t *testing.T, p *Proxy, h Handler) string { 25 t.Helper() 26 ln, err := net.Listen("tcp", "127.0.0.1:0") 27 if err != nil { 28 t.Fatalf("listen: %v", err) 29 } 30 t.Cleanup(func() { ln.Close() }) 31 32 go func() { 33 for { 34 conn, err := ln.Accept() 35 if err != nil { 36 return 37 } 38 go h(conn, p) 39 } 40 }() 41 42 return ln.Addr().String() 43 } 44 45 func writeTempCertKey(t *testing.T) (certFile, keyFile string, pool *x509.CertPool) { 46 t.Helper() 47 48 pool, tlsCert := generateSelfSignedCert(t) 49 50 cf, err := os.CreateTemp("", "mic-cert-*.pem") 51 if err != nil { 52 t.Fatalf("creating cert temp file: %v", err) 53 } 54 pem.Encode(cf, &pem.Block{Type: "CERTIFICATE", Bytes: tlsCert.Certificate[0]}) 55 cf.Close() 56 certFile = cf.Name() 57 58 // PKCS8 format is required: tls.LoadX509KeyPair accepts "PRIVATE KEY" blocks. 59 keyBytes, err := x509.MarshalPKCS8PrivateKey(tlsCert.PrivateKey) 60 if err != nil { 61 t.Fatalf("marshaling private key: %v", err) 62 } 63 kf, err := os.CreateTemp("", "mic-key-*.pem") 64 if err != nil { 65 t.Fatalf("creating key temp file: %v", err) 66 } 67 pem.Encode(kf, &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes}) 68 kf.Close() 69 keyFile = kf.Name() 70 71 t.Cleanup(func() { 72 os.Remove(certFile) 73 os.Remove(keyFile) 74 }) 75 76 return certFile, keyFile, pool 77 } 78 79 // client → TCP CONNECT → proxy → uTLS(Chrome120) → httptest.TLSServer 80 func TestClientFront_Integration(t *testing.T) { 81 ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 82 w.Header().Set("Connection", "close") 83 w.WriteHeader(http.StatusOK) 84 fmt.Fprintln(w, "client-front integration OK") 85 })) 86 defer ts.Close() 87 88 caPool := ts.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs 89 90 fp, err := fingerprint.ByName("chrome-120") 91 if err != nil { 92 t.Fatalf("fingerprint.ByName: %v", err) 93 } 94 95 p := &Proxy{CAPool: caPool, Fingerprint: fp} 96 proxyAddr := serveHandler(t, p, HttpsHandler) 97 98 conn, err := net.DialTimeout("tcp", proxyAddr, testTimeout) 99 if err != nil { 100 t.Fatalf("dial proxy: %v", err) 101 } 102 defer conn.Close() 103 conn.SetDeadline(time.Now().Add(testTimeout)) 104 105 targetAddr := ts.Listener.Addr().String() 106 fmt.Fprintf(conn, "CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", targetAddr, targetAddr) 107 108 reader := bufio.NewReader(conn) 109 resp, err := http.ReadResponse(reader, nil) 110 if err != nil { 111 t.Fatalf("reading CONNECT response: %v", err) 112 } 113 if resp.StatusCode != http.StatusOK { 114 t.Fatalf("CONNECT status: %d; want 200", resp.StatusCode) 115 } 116 117 // The proxy already handshaked uTLS to the target, so these raw bytes travel 118 // inside that TLS connection. 119 fmt.Fprintf(conn, "GET / HTTP/1.1\r\nHost: %s\r\nConnection: close\r\n\r\n", targetAddr) 120 121 resp, err = http.ReadResponse(reader, nil) 122 if err != nil { 123 t.Fatalf("reading GET response: %v", err) 124 } 125 defer resp.Body.Close() 126 127 if resp.StatusCode != http.StatusOK { 128 t.Errorf("GET status: %d; want 200", resp.StatusCode) 129 } 130 body, _ := io.ReadAll(resp.Body) 131 t.Logf("response body: %s", body) 132 } 133 134 // client → TLS → proxy (terminates TLS) → uTLS(Chrome120) → httptest.TLSServer 135 func TestServerFront_Integration(t *testing.T) { 136 backend := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 137 w.Header().Set("Connection", "close") 138 w.WriteHeader(http.StatusOK) 139 fmt.Fprintln(w, "server-front integration OK") 140 })) 141 defer backend.Close() 142 143 backendCAPool := backend.Client().Transport.(*http.Transport).TLSClientConfig.RootCAs 144 certFile, keyFile, proxyCertPool := writeTempCertKey(t) 145 146 fp, err := fingerprint.ByName("chrome-120") 147 if err != nil { 148 t.Fatalf("fingerprint.ByName: %v", err) 149 } 150 151 p := &Proxy{ 152 BackendAddr: backend.Listener.Addr().String(), 153 CAPool: backendCAPool, 154 Fingerprint: fp, 155 } 156 handler, err := ServerFrontHandler(certFile, keyFile) 157 if err != nil { 158 t.Fatalf("ServerFrontHandler: %v", err) 159 } 160 proxyAddr := serveHandler(t, p, handler) 161 162 tlsConn, err := tls.DialWithDialer( 163 &net.Dialer{Timeout: testTimeout}, 164 "tcp", proxyAddr, 165 &tls.Config{RootCAs: proxyCertPool, ServerName: "localhost"}, 166 ) 167 if err != nil { 168 t.Fatalf("tls.Dial to proxy: %v", err) 169 } 170 defer tlsConn.Close() 171 tlsConn.SetDeadline(time.Now().Add(testTimeout)) 172 173 fmt.Fprintf(tlsConn, "GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n") 174 175 reader := bufio.NewReader(tlsConn) 176 resp, err := http.ReadResponse(reader, nil) 177 if err != nil { 178 t.Fatalf("reading response: %v", err) 179 } 180 defer resp.Body.Close() 181 182 if resp.StatusCode != http.StatusOK { 183 t.Errorf("status: %d; want 200", resp.StatusCode) 184 } 185 body, _ := io.ReadAll(resp.Body) 186 t.Logf("response body: %s", body) 187 }