641 lines · 22.6 KB · Go
RawDownload
package helps import (	"bytes"	"context"	"crypto/md5"	"encoding/binary"	"encoding/hex"	"errors"	"fmt"	"io"	"net"	"net/http"	"os"	"reflect"	"strconv"	"strings"	"sync/atomic"	"testing"	"time" 	tls "github.com/refraction-networking/utls"	cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth") type utlsClientRoundTripFunc func(*http.Request) (*http.Response, error) func (f utlsClientRoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {	return f(req)} type trackedReadCloser struct {	io.Reader	closeCount int	closeErr   error	onClose    func()} func (r *trackedReadCloser) Close() error {	r.closeCount++	if r.onClose != nil {		r.onClose()	}	return r.closeErr} type contextDialerFunc func(context.Context, string, string) (net.Conn, error) func (f contextDialerFunc) Dial(network, addr string) (net.Conn, error) {	return f(context.Background(), network, addr)} func (f contextDialerFunc) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {	return f(ctx, network, addr)} type trackedNetConn struct {	net.Conn	closeCount atomic.Int32} func (c *trackedNetConn) Close() error {	c.closeCount.Add(1)	return c.Conn.Close()} func TestCloseConnectionBodyClosesConnectionBeforeBodyOnce(t *testing.T) {	bodyErr := errors.New("body close failed")	connectionErr := errors.New("connection close failed")	var closeOrder []string	body := &trackedReadCloser{		Reader:   strings.NewReader("response"),		closeErr: bodyErr,		onClose: func() {			closeOrder = append(closeOrder, "body")		},	}	connectionCloseCount := 0	wrapped := &closeConnectionBody{		ReadCloser: body,		closeConnection: func() error {			connectionCloseCount++			closeOrder = append(closeOrder, "connection")			return connectionErr		},	} 	payload, errRead := io.ReadAll(wrapped)	if errRead != nil {		t.Fatal(errRead)	}	if got, want := string(payload), "response"; got != want {		t.Fatalf("response body = %q, want %q", got, want)	} 	errClose := wrapped.Close()	if !errors.Is(errClose, bodyErr) {		t.Fatalf("close error = %v, want body close error", errClose)	}	if !errors.Is(errClose, connectionErr) {		t.Fatalf("close error = %v, want connection close error", errClose)	}	if errCloseAgain := wrapped.Close(); errCloseAgain != errClose {		t.Fatalf("second close error = %v, want %v", errCloseAgain, errClose)	}	if body.closeCount != 1 {		t.Fatalf("body close count = %d, want 1", body.closeCount)	}	if connectionCloseCount != 1 {		t.Fatalf("connection close count = %d, want 1", connectionCloseCount)	}	if want := []string{"connection", "body"}; !reflect.DeepEqual(closeOrder, want) {		t.Fatalf("close order = %v, want %v", closeOrder, want)	}} func TestUtlsRoundTripperDialUsesRequestContext(t *testing.T) {	dialStarted := make(chan struct{})	roundTripper := &utlsRoundTripper{dialer: contextDialerFunc(func(ctx context.Context, _, _ string) (net.Conn, error) {		close(dialStarted)		<-ctx.Done()		return nil, ctx.Err()	})}	ctx, cancel := context.WithCancel(t.Context())	req, errRequest := http.NewRequestWithContext(ctx, http.MethodGet, "https://chatgpt.com/backend-api/codex/responses", nil)	if errRequest != nil {		t.Fatal(errRequest)	}	roundTripDone := make(chan error, 1)	go func() {		resp, errRoundTrip := roundTripper.RoundTrip(req)		if resp != nil && resp.Body != nil {			errRoundTrip = errors.Join(errRoundTrip, resp.Body.Close())		}		roundTripDone <- errRoundTrip	}() 	select {	case <-dialStarted:	case <-time.After(time.Second):		t.Fatal("dial did not start")	}	cancel()	select {	case errRoundTrip := <-roundTripDone:		if !errors.Is(errRoundTrip, context.Canceled) {			t.Fatalf("RoundTrip error = %v, want context canceled", errRoundTrip)		}	case <-time.After(time.Second):		t.Fatal("RoundTrip did not stop after context cancellation")	}} func TestUtlsRoundTripperHandshakeUsesRequestContext(t *testing.T) {	clientConn, serverConn := net.Pipe()	t.Cleanup(func() {		if errClose := clientConn.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) && !errors.Is(errClose, io.ErrClosedPipe) {			t.Errorf("close client connection: %v", errClose)		}		if errClose := serverConn.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) && !errors.Is(errClose, io.ErrClosedPipe) {			t.Errorf("close server connection: %v", errClose)		}	}) 	trackedConn := &trackedNetConn{Conn: clientConn}	dialDone := make(chan struct{})	roundTripper := &utlsRoundTripper{dialer: contextDialerFunc(func(context.Context, string, string) (net.Conn, error) {		close(dialDone)		return trackedConn, nil	})}	ctx, cancel := context.WithCancel(t.Context())	connectionDone := make(chan error, 1)	go func() {		h2Conn, errConnect := roundTripper.createConnection(ctx, "chatgpt.com", "chatgpt.com:443")		if h2Conn != nil {			errConnect = errors.Join(errConnect, h2Conn.Close())		}		connectionDone <- errConnect	}() 	select {	case <-dialDone:	case <-time.After(time.Second):		t.Fatal("dial did not complete")	}	cancel()	select {	case errConnect := <-connectionDone:		if !errors.Is(errConnect, context.Canceled) {			t.Fatalf("createConnection error = %v, want context canceled", errConnect)		}	case <-time.After(time.Second):		t.Fatal("TLS handshake did not stop after context cancellation")	}	if got := trackedConn.closeCount.Load(); got != 1 {		t.Fatalf("connection close count = %d, want 1", got)	}} type claudeCodeTLSFingerprintFixture struct {	ClientHelloLength   int	JA3                 string	JA3MD5              string	ALPN                []string	HTTPVersion         string	CipherSuites        []uint16	ExtensionTypes      []uint16	ExtensionLengths    [][2]int	SupportedGroups     []uint16	PointFormats        []uint8	SignatureAlgorithms []uint16	SupportedVersions   []uint16	KeyShareGroups      []uint16} func TestClaudeCodeTLSClientHelloSpecMatches220Capture(t *testing.T) {	t.Parallel() 	fixture := claudeCodeTLSFingerprintFixture{		ClientHelloLength: 508,		JA3:               "771,4865-4866-4867-49195-49199-49196-49200-52393-52392-49161-49171-49162-49172-156-157-47-53,0-23-65281-10-11-35-16-5-13-18-51-45-43-21,29-23-24,0",		JA3MD5:            "d871d02cecbde59abbf8f4806134addf",		ALPN:              []string{"http/1.1"},		HTTPVersion:       "HTTP/1.1",		CipherSuites:      []uint16{4865, 4866, 4867, 49195, 49199, 49196, 49200, 52393, 52392, 49161, 49171, 49162, 49172, 156, 157, 47, 53},		ExtensionTypes:    []uint16{0, 23, 65281, 10, 11, 35, 16, 5, 13, 18, 51, 45, 43, 21},		ExtensionLengths: [][2]int{			{0, 22}, {23, 0}, {65281, 1}, {10, 8}, {11, 2}, {35, 0}, {16, 11},			{5, 5}, {13, 20}, {18, 0}, {51, 38}, {45, 2}, {43, 5}, {21, 231},		},		SupportedGroups:     []uint16{29, 23, 24},		PointFormats:        []uint8{0},		SignatureAlgorithms: []uint16{1027, 2052, 1025, 1283, 2053, 1281, 2054, 1537, 513},		SupportedVersions:   []uint16{772, 771},		KeyShareGroups:      []uint16{29},	} 	record := captureClaudeCodeClientHello(t)	if got := len(record) - 9; got != fixture.ClientHelloLength {		t.Fatalf("ClientHello length = %d, want %d", got, fixture.ClientHelloLength)	}	if got := parseClientHelloExtensionLengths(t, record); !reflect.DeepEqual(got, fixture.ExtensionLengths) {		t.Fatalf("extension lengths = %v, want %v", got, fixture.ExtensionLengths)	} 	spec, errFingerprint := (&tls.Fingerprinter{}).FingerprintClientHello(record)	if errFingerprint != nil {		t.Fatal(errFingerprint)	}	actual := summarizeClaudeCodeClientHelloSpec(t, spec)	if !reflect.DeepEqual(actual.CipherSuites, fixture.CipherSuites) {		t.Fatalf("cipher suites = %v, want %v", actual.CipherSuites, fixture.CipherSuites)	}	if !reflect.DeepEqual(actual.ExtensionTypes, fixture.ExtensionTypes) {		t.Fatalf("extension types = %v, want %v", actual.ExtensionTypes, fixture.ExtensionTypes)	}	if !reflect.DeepEqual(actual.ALPN, fixture.ALPN) {		t.Fatalf("ALPN = %v, want %v", actual.ALPN, fixture.ALPN)	}	if !reflect.DeepEqual(actual.SupportedGroups, fixture.SupportedGroups) {		t.Fatalf("supported groups = %v, want %v", actual.SupportedGroups, fixture.SupportedGroups)	}	if !reflect.DeepEqual(actual.PointFormats, fixture.PointFormats) {		t.Fatalf("point formats = %v, want %v", actual.PointFormats, fixture.PointFormats)	}	if !reflect.DeepEqual(actual.SignatureAlgorithms, fixture.SignatureAlgorithms) {		t.Fatalf("signature algorithms = %v, want %v", actual.SignatureAlgorithms, fixture.SignatureAlgorithms)	}	if !reflect.DeepEqual(actual.SupportedVersions, fixture.SupportedVersions) {		t.Fatalf("supported versions = %v, want %v", actual.SupportedVersions, fixture.SupportedVersions)	}	if !reflect.DeepEqual(actual.KeyShareGroups, fixture.KeyShareGroups) {		t.Fatalf("key share groups = %v, want %v", actual.KeyShareGroups, fixture.KeyShareGroups)	}	if actual.JA3 != fixture.JA3 || actual.JA3MD5 != fixture.JA3MD5 {		t.Fatalf("JA3 = %q (%s), want %q (%s)", actual.JA3, actual.JA3MD5, fixture.JA3, fixture.JA3MD5)	} 	transport, ok := newClaudeCodeRoundTripper("").(*http.Transport)	if !ok {		t.Fatalf("Claude Code transport type = %T, want *http.Transport", newClaudeCodeRoundTripper(""))	}	if transport.ForceAttemptHTTP2 {		t.Fatal("Claude Code transport must not force HTTP/2")	}	if fixture.HTTPVersion != "HTTP/1.1" {		t.Fatalf("fixture HTTP version = %q, want HTTP/1.1", fixture.HTTPVersion)	}} func TestClaudeCodeTLSResumptionIsWireSafe(t *testing.T) {	t.Parallel() 	// RFC 8446 4.2.11 requires pre_shared_key to be the final extension, after	// the padding extension.	spec := claudeCodeTLSClientHelloSpec()	last := spec.Extensions[len(spec.Extensions)-1]	if _, ok := last.(*tls.UtlsPreSharedKeyExtension); !ok {		t.Fatalf("last inference extension = %T, want *tls.UtlsPreSharedKeyExtension", last)	}	if _, ok := spec.Extensions[len(spec.Extensions)-2].(*tls.UtlsPaddingExtension); !ok {		t.Fatalf("extension before pre_shared_key = %T, want *tls.UtlsPaddingExtension", spec.Extensions[len(spec.Extensions)-2])	} 	// Without OmitEmptyPsk uTLS refuses to marshal an empty PSK, and without	// PreferSkipResumptionOnNilExtension a HelloCustom resumption attempt panics.	cfg := newClaudeCodeTLSConfig("api.anthropic.com", tls.NewLRUClientSessionCache(claudeCodeSessionCacheCapacity))	if cfg.ClientSessionCache == nil {		t.Fatal("ClientSessionCache = nil, want a session cache so resumption is possible")	}	if !cfg.OmitEmptyPsk {		t.Fatal("OmitEmptyPsk = false, want true so an unresumed ClientHello stays byte-identical")	}	if !cfg.PreferSkipResumptionOnNilExtension {		t.Fatal("PreferSkipResumptionOnNilExtension = false, want true to avoid a HelloCustom resumption panic")	}} func TestClaudeCodeRequestHeaderOrderMatchesNative220Capture(t *testing.T) {	t.Parallel() 	if got, want := claudeCodeRequestHeaderOrder(http.MethodPost, "/v1/messages?beta=true"), claudeCodeMessagesHeaderOrder; !reflect.DeepEqual(got, want) {		t.Fatalf("Messages header order = %v, want %v", got, want)	}	if got, want := claudeCodeRequestHeaderOrder(http.MethodPost, "/v1/messages/count_tokens?beta=true"), claudeCodeCountTokensHeaderOrder; !reflect.DeepEqual(got, want) {		t.Fatalf("count_tokens header order = %v, want %v", got, want)	}	for _, name := range claudeCodeCountTokensHeaderOrder {		if name == "X-Stainless-Timeout" {			t.Fatal("count_tokens header order unexpectedly contains X-Stainless-Timeout")		}	}} func TestCachedClaudeCodeRoundTripperReusesTransport(t *testing.T) {	t.Parallel() 	const proxyURL = "http://127.0.0.1:29653"	first := cachedClaudeCodeRoundTripper(proxyURL)	second := cachedClaudeCodeRoundTripper(proxyURL)	if first != second {		t.Fatal("Claude Code transport cache returned different transports for one proxy")	}} func TestCachedClaudeCodeRoundTripperBoundsProxyCardinality(t *testing.T) {	firstProxy := fmt.Sprintf("http://127.0.0.1:%d", 30000)	first := cachedClaudeCodeRoundTripper(firstProxy)	for index := 1; index <= claudeCodeRoundTripperCacheCapacity; index++ {		cachedClaudeCodeRoundTripper(fmt.Sprintf("http://127.0.0.1:%d", 30000+index))	}	if got := claudeCodeRoundTripperCache.Len(); got > claudeCodeRoundTripperCacheCapacity {		t.Fatalf("transport cache entries = %d, want at most %d", got, claudeCodeRoundTripperCacheCapacity)	}	if recreated := cachedClaudeCodeRoundTripper(firstProxy); recreated == first {		t.Fatal("least recently used proxy transport was not evicted")	}} func TestClaudeCodeTLSClientHelloCapture(t *testing.T) {	proxyURL := os.Getenv("CPA_TLS_FP_PROXY")	if proxyURL == "" {		t.Skip("CPA_TLS_FP_PROXY is not set")	} 	client := NewUtlsHTTPClient(t.Context(), nil, &cliproxyauth.Auth{ProxyURL: proxyURL}, 0)	req, errRequest := http.NewRequestWithContext(t.Context(), http.MethodPost, "https://api.anthropic.com/v1/messages", bytes.NewBufferString(`{"model":"claude-opus-4-6","max_tokens":1,"messages":[{"role":"user","content":"x"}]}`))	if errRequest != nil {		t.Fatal(errRequest)	}	req.Header.Set("Content-Type", "application/json")	req.Header.Set("x-api-key", "dummy-tls-fingerprint")	resp, errDo := client.Do(req)	if errDo != nil {		t.Fatal(errDo)	}	if errClose := resp.Body.Close(); errClose != nil {		t.Fatal(errClose)	}} func TestFallbackRoundTripperSelectsProviderFingerprint(t *testing.T) {	t.Parallel() 	route := func(label string) http.RoundTripper {		return utlsClientRoundTripFunc(func(req *http.Request) (*http.Response, error) {			return &http.Response{				StatusCode: http.StatusOK,				Header:     http.Header{"X-Test-Route": []string{label}},				Body:       io.NopCloser(strings.NewReader("{}")),				Request:    req,			}, nil		})	}	roundTripper := &fallbackRoundTripper{		anthropic: route("anthropic"),		chrome:    route("chrome"),		fallback:  route("fallback"),	}	tests := []struct {		name string		url  string		want string	}{		{name: "Anthropic HTTPS", url: "https://api.anthropic.com/v1/messages", want: "anthropic"},		{name: "Anthropic explicit HTTPS port", url: "https://api.anthropic.com:443/v1/messages", want: "anthropic"},		{name: "Anthropic custom port", url: "https://api.anthropic.com:8443/v1/messages", want: "fallback"},		{name: "Anthropic userinfo", url: "https://caller@api.anthropic.com/v1/messages", want: "fallback"},		{name: "Anthropic lookalike", url: "https://api.anthropic.com.example/v1/messages", want: "fallback"},		{name: "ChatGPT HTTPS", url: "https://chatgpt.com/backend-api/codex/responses", want: "chrome"},		{name: "Other HTTPS", url: "https://example.com/v1/messages", want: "fallback"},		{name: "Anthropic HTTP", url: "http://api.anthropic.com/v1/messages", want: "fallback"},	}	for _, tt := range tests {		t.Run(tt.name, func(t *testing.T) {			req, errRequest := http.NewRequest(http.MethodGet, tt.url, nil)			if errRequest != nil {				t.Fatal(errRequest)			}			resp, errRoundTrip := roundTripper.RoundTrip(req)			if errRoundTrip != nil {				t.Fatal(errRoundTrip)			}			defer func() {				if errClose := resp.Body.Close(); errClose != nil {					t.Errorf("close response body: %v", errClose)				}			}()			if got := resp.Header.Get("X-Test-Route"); got != tt.want {				t.Fatalf("route = %q, want %q", got, tt.want)			}		})	}} func TestNewUtlsHTTPClientUsesContextRoundTripperForProtectedHost(t *testing.T) {	t.Parallel() 	for _, targetURL := range []string{		"https://api.anthropic.com/v1/messages",		"https://chatgpt.com/backend-api/codex/responses",	} {		t.Run(targetURL, func(t *testing.T) {			called := false			ctx := context.WithValue(context.Background(), "cliproxy.roundtripper", utlsClientRoundTripFunc(func(req *http.Request) (*http.Response, error) {				called = true				return &http.Response{					StatusCode: http.StatusOK,					Header:     make(http.Header),					Body:       io.NopCloser(strings.NewReader("{}")),					Request:    req,				}, nil			})) 			client := NewUtlsHTTPClient(ctx, nil, nil, 0)			resp, err := client.Get(targetURL)			if err != nil {				t.Fatalf("client.Get returned error: %v", err)			}			if errClose := resp.Body.Close(); errClose != nil {				t.Fatalf("response body close returned error: %v", errClose)			}			if !called {				t.Fatal("expected context RoundTripper to handle protected host request")			}		})	}} type claudeCodeClientHelloSummary struct {	CipherSuites        []uint16	ExtensionTypes      []uint16	ALPN                []string	SupportedGroups     []uint16	PointFormats        []uint8	SignatureAlgorithms []uint16	SupportedVersions   []uint16	KeyShareGroups      []uint16	JA3                 string	JA3MD5              string} func captureClaudeCodeClientHello(t *testing.T) []byte {	t.Helper() 	clientConn, serverConn := net.Pipe()	t.Cleanup(func() {		if errClose := clientConn.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) {			t.Errorf("close client pipe: %v", errClose)		}		if errClose := serverConn.Close(); errClose != nil && !errors.Is(errClose, net.ErrClosed) {			t.Errorf("close server pipe: %v", errClose)		}	})	// Use the production config so the captured bytes reflect the real dial path,	// including the resumption settings.	cfg := newClaudeCodeTLSConfig("api.anthropic.com", tls.NewLRUClientSessionCache(claudeCodeSessionCacheCapacity))	tlsConn := tls.UClient(clientConn, cfg, tls.HelloCustom)	if errPreset := tlsConn.ApplyPreset(claudeCodeTLSClientHelloSpec()); errPreset != nil {		t.Fatal(errPreset)

Lines 1–500 of 641.