package helps import ( "context" "errors" "fmt" "io" "net" "net/http" "strings" "sync" "time" tls "github.com/refraction-networking/utls" internalcache "github.com/router-for-me/CLIProxyAPI/v7/internal/cache" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" "github.com/router-for-me/CLIProxyAPI/v7/internal/httpwire" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth" "github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil" log "github.com/sirupsen/logrus" "golang.org/x/net/http2" "golang.org/x/net/proxy") // utlsRoundTripper implements http.RoundTripper using a Chrome fingerprint for// providers that require a browser-like TLS and HTTP/2 transport. Each request// gets a dedicated connection that is closed with the response body.type utlsRoundTripper struct { dialer proxy.Dialer} type closeConnectionBody struct { io.ReadCloser closeConnection func() error once sync.Once err error} func (b *closeConnectionBody) Close() error { if b == nil { return nil } b.once.Do(func() { var errConnection error if b.closeConnection != nil { errConnection = b.closeConnection() } var errBody error if b.ReadCloser != nil { errBody = b.ReadCloser.Close() } b.err = errors.Join(errBody, errConnection) }) return b.err} func newUtlsRoundTripper(proxyURL string) *utlsRoundTripper { var dialer proxy.Dialer = proxy.Direct if proxyURL != "" { proxyDialer, mode, errBuild := proxyutil.BuildDialer(proxyURL) if errBuild != nil { log.Errorf("utls: failed to configure proxy dialer for %q: %v", proxyutil.Redact(proxyURL), errBuild) } else if mode != proxyutil.ModeInherit && proxyDialer != nil { dialer = proxyDialer } } return &utlsRoundTripper{dialer: dialer}} func (t *utlsRoundTripper) createConnection(ctx context.Context, host, addr string) (*http2.ClientConn, error) { contextDialer, ok := t.dialer.(proxy.ContextDialer) if !ok { return nil, fmt.Errorf("utls: dialer does not support context cancellation") } conn, errDial := contextDialer.DialContext(ctx, "tcp", addr) if errDial != nil { return nil, fmt.Errorf("utls: dial upstream: %w", errDial) } tlsConfig := &tls.Config{ServerName: host} tlsConn := tls.UClient(conn, tlsConfig, tls.HelloChrome_Auto) if errHandshake := tlsConn.HandshakeContext(ctx); errHandshake != nil { if errors.Is(errHandshake, context.Canceled) || errors.Is(errHandshake, context.DeadlineExceeded) { return nil, fmt.Errorf("utls: TLS handshake: %w", errHandshake) } if errClose := conn.Close(); errClose != nil { return nil, fmt.Errorf("utls: TLS handshake: %w; close connection: %v", errHandshake, errClose) } return nil, fmt.Errorf("utls: TLS handshake: %w", errHandshake) } tr := &http2.Transport{} h2Conn, errClientConn := tr.NewClientConn(tlsConn) if errClientConn != nil { if errClose := tlsConn.Close(); errClose != nil { return nil, fmt.Errorf("utls: initialize HTTP/2 connection: %w; close TLS connection: %v", errClientConn, errClose) } return nil, fmt.Errorf("utls: initialize HTTP/2 connection: %w", errClientConn) } return h2Conn, nil} func (t *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { hostname := req.URL.Hostname() port := req.URL.Port() if port == "" { port = "443" } addr := net.JoinHostPort(hostname, port) h2Conn, err := t.createConnection(req.Context(), hostname, addr) if err != nil { return nil, err } resp, err := h2Conn.RoundTrip(req) if err != nil { if errClose := h2Conn.Close(); errClose != nil { log.Debugf("utls: close connection after round trip failure: %v", errClose) } return nil, err } if resp == nil { if errClose := h2Conn.Close(); errClose != nil { log.Debugf("utls: close connection after empty response: %v", errClose) } return nil, fmt.Errorf("utls: upstream returned an empty response") } if resp.Body == nil { resp.Body = http.NoBody } resp.Body = &closeConnectionBody{ ReadCloser: resp.Body, closeConnection: h2Conn.Close, } return resp, nil} // claudeCodeSessionCacheCapacity bounds the per-transport TLS session cache for// the Anthropic inference plane.const claudeCodeSessionCacheCapacity = 32 // newClaudeCodeTLSConfig builds the uTLS config for one inference-plane dial.//// OmitEmptyPsk keeps the pre_shared_key extension silent until a session is// cached, so an unresumed ClientHello stays byte-identical to the captured// native handshake. PreferSkipResumptionOnNilExtension turns uTLS's HelloCustom// "resume without the matching extension" panic into a skipped resumption.func newClaudeCodeTLSConfig(host string, sessionCache tls.ClientSessionCache) *tls.Config { return &tls.Config{ ServerName: host, ClientSessionCache: sessionCache, OmitEmptyPsk: true, PreferSkipResumptionOnNilExtension: true, }} // claudeCodeTLSClientHelloSpec reproduces the deterministic Node/OpenSSL// ClientHello emitted by Claude Code 2.1.220 on macOS arm64. Keep this spec in// sync with a fresh native capture whenever the advertised Claude Code version// changes.func claudeCodeTLSClientHelloSpec() *tls.ClientHelloSpec { return &tls.ClientHelloSpec{ CipherSuites: []uint16{ tls.TLS_AES_128_GCM_SHA256, tls.TLS_AES_256_GCM_SHA384, tls.TLS_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, tls.TLS_RSA_WITH_AES_128_GCM_SHA256, tls.TLS_RSA_WITH_AES_256_GCM_SHA384, tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_AES_256_CBC_SHA, }, CompressionMethods: []uint8{0}, Extensions: []tls.TLSExtension{ &tls.SNIExtension{}, &tls.ExtendedMasterSecretExtension{}, &tls.RenegotiationInfoExtension{Renegotiation: tls.RenegotiateOnceAsClient}, &tls.SupportedCurvesExtension{Curves: []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384}}, &tls.SupportedPointsExtension{SupportedPoints: []byte{0}}, &tls.SessionTicketExtension{}, &tls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}}, &tls.StatusRequestExtension{}, &tls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []tls.SignatureScheme{ tls.ECDSAWithP256AndSHA256, tls.PSSWithSHA256, tls.PKCS1WithSHA256, tls.ECDSAWithP384AndSHA384, tls.PSSWithSHA384, tls.PKCS1WithSHA384, tls.PSSWithSHA512, tls.PKCS1WithSHA512, tls.PKCS1WithSHA1, }}, &tls.SCTExtension{}, &tls.KeyShareExtension{KeyShares: []tls.KeyShare{{Group: tls.X25519}}}, &tls.PSKKeyExchangeModesExtension{Modes: []uint8{tls.PskModeDHE}}, &tls.SupportedVersionsExtension{Versions: []uint16{tls.VersionTLS13, tls.VersionTLS12}}, &tls.UtlsPaddingExtension{GetPaddingLen: tls.BoringPaddingStyle}, // pre_shared_key MUST be the final extension (RFC 8446 4.2.11), after // padding. It contributes zero bytes until a cached session exists. &tls.UtlsPreSharedKeyExtension{}, }, }} const claudeCodeRoundTripperCacheCapacity = 64 var claudeCodeRoundTripperCache = internalcache.NewBoundedLRU[string, http.RoundTripper]( claudeCodeRoundTripperCacheCapacity, func(_ string, roundTripper http.RoundTripper) { if transport, ok := roundTripper.(interface{ CloseIdleConnections() }); ok { transport.CloseIdleConnections() } },) var claudeCodeMessagesHeaderOrder = []string{ "Accept", "Authorization", "Content-Type", "User-Agent", "X-Claude-Code-Session-Id", "X-Stainless-Arch", "X-Stainless-Lang", "X-Stainless-OS", "X-Stainless-Package-Version", "X-Stainless-Retry-Count", "X-Stainless-Runtime", "X-Stainless-Runtime-Version", "X-Stainless-Timeout", "anthropic-beta", "anthropic-dangerous-direct-browser-access", "anthropic-version", "x-app", "x-client-request-id", "Connection", "Host", "Accept-Encoding", "Content-Length",} var claudeCodeCountTokensHeaderOrder = []string{ "Accept", "Authorization", "Content-Type", "User-Agent", "X-Claude-Code-Session-Id", "X-Stainless-Arch", "X-Stainless-Lang", "X-Stainless-OS", "X-Stainless-Package-Version", "X-Stainless-Retry-Count", "X-Stainless-Runtime", "X-Stainless-Runtime-Version", "anthropic-beta", "anthropic-dangerous-direct-browser-access", "anthropic-version", "x-app", "x-client-request-id", "Connection", "Host", "Accept-Encoding", "Content-Length",} func claudeCodeRequestHeaderOrder(_, requestTarget string) []string { if strings.HasPrefix(requestTarget, "/v1/messages/count_tokens") { return claudeCodeCountTokensHeaderOrder } return claudeCodeMessagesHeaderOrder} func cachedClaudeCodeRoundTripper(proxyURL string) http.RoundTripper { return claudeCodeRoundTripperCache.GetOrAdd(proxyURL, func() http.RoundTripper { return newClaudeCodeRoundTripper(proxyURL) })} func newClaudeCodeRoundTripper(proxyURL string) http.RoundTripper { // The cache is scoped to this round tripper, which is already keyed by proxy, // so resumption never crosses proxy boundaries. sessionCache := tls.NewLRUClientSessionCache(claudeCodeSessionCacheCapacity) var dialer proxy.Dialer = proxy.Direct if proxyURL != "" { proxyDialer, mode, errBuild := proxyutil.BuildDialer(proxyURL) if errBuild != nil { log.Errorf("claude tls: failed to configure proxy dialer for %q: %v", proxyutil.Redact(proxyURL), errBuild) } else if mode != proxyutil.ModeInherit && proxyDialer != nil { dialer = proxyDialer } } transport := &http.Transport{ ForceAttemptHTTP2: false, DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { var ( conn net.Conn err error ) if contextDialer, ok := dialer.(proxy.ContextDialer); ok { conn, err = contextDialer.DialContext(ctx, network, addr) } else { conn, err = dialer.Dial(network, addr) } if err != nil { return nil, fmt.Errorf("claude tls: dial upstream: %w", err) } host, _, errSplit := net.SplitHostPort(addr) if errSplit != nil { if errClose := conn.Close(); errClose != nil { log.Debugf("claude tls: close failed connection: %v", errClose) } return nil, fmt.Errorf("claude tls: split upstream address: %w", errSplit) } tlsConn := tls.UClient(conn, newClaudeCodeTLSConfig(host, sessionCache), tls.HelloCustom) if errPreset := tlsConn.ApplyPreset(claudeCodeTLSClientHelloSpec()); errPreset != nil { if errClose := tlsConn.Close(); errClose != nil { log.Debugf("claude tls: close connection after preset failure: %v", errClose) } return nil, fmt.Errorf("claude tls: apply Claude Code ClientHello: %w", errPreset) } if errHandshake := tlsConn.HandshakeContext(ctx); errHandshake != nil { if errClose := tlsConn.Close(); errClose != nil { log.Debugf("claude tls: close connection after handshake failure: %v", errClose) } return nil, fmt.Errorf("claude tls: handshake upstream: %w", errHandshake) } return httpwire.NewOrderedRequestConn(tlsConn, claudeCodeRequestHeaderOrder), nil }, } return transport} // fallbackRoundTripper uses provider-specific TLS fingerprints for protected// HTTPS hosts and falls back to the standard transport for all other requests.type fallbackRoundTripper struct { anthropic http.RoundTripper chrome http.RoundTripper fallback http.RoundTripper} func (f *fallbackRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { if IsAnthropicUpstreamURL(req.URL) { return f.anthropic.RoundTrip(req) } if req.URL.Scheme == "https" && strings.EqualFold(req.URL.Hostname(), "chatgpt.com") { return f.chrome.RoundTrip(req) } return f.fallback.RoundTrip(req)} // NewUtlsHTTPClient creates an HTTP client using provider-specific TLS// fingerprints for protected hosts. It uses Claude Code's Node/OpenSSL profile// for Anthropic and a Chrome profile for ChatGPT, with a standard-transport// fallback for other hosts.func NewUtlsHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client { var proxyURL string if auth != nil { proxyURL = strings.TrimSpace(auth.ProxyURL) } if proxyURL == "" && cfg != nil { proxyURL = strings.TrimSpace(cfg.ProxyURL) } var ctxRoundTripper http.RoundTripper if ctx != nil { ctxRoundTripper, _ = ctx.Value("cliproxy.roundtripper").(http.RoundTripper) } var chromeRT http.RoundTripper = newUtlsRoundTripper(proxyURL) var anthropicRT http.RoundTripper = cachedClaudeCodeRoundTripper(proxyURL) var standardTransport http.RoundTripper = http.DefaultTransport if proxyURL != "" { if transport := buildProxyTransport(proxyURL); transport != nil { standardTransport = transport } } else if ctxRoundTripper != nil { chromeRT = ctxRoundTripper anthropicRT = ctxRoundTripper standardTransport = ctxRoundTripper } client := &http.Client{ Transport: &fallbackRoundTripper{ anthropic: anthropicRT, chrome: chromeRT, fallback: standardTransport, }, } if timeout > 0 { client.Timeout = timeout } return client}
package helps
import (
"context"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"sync"
"time"
tls "github.com/refraction-networking/utls"
internalcache "github.com/router-for-me/CLIProxyAPI/v7/internal/cache"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
"github.com/router-for-me/CLIProxyAPI/v7/internal/httpwire"
cliproxyauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
"github.com/router-for-me/CLIProxyAPI/v7/sdk/proxyutil"
log "github.com/sirupsen/logrus"
"golang.org/x/net/http2"
"golang.org/x/net/proxy"
)
// utlsRoundTripper implements http.RoundTripper using a Chrome fingerprint for
// providers that require a browser-like TLS and HTTP/2 transport. Each request
// gets a dedicated connection that is closed with the response body.
type utlsRoundTripper struct {
dialer proxy.Dialer
}
type closeConnectionBody struct {
io.ReadCloser
closeConnection func() error
once sync.Once
err error
}
func (b *closeConnectionBody) Close() error {
if b == nil {
return nil
}
b.once.Do(func() {
var errConnection error
if b.closeConnection != nil {
errConnection = b.closeConnection()
}
var errBody error
if b.ReadCloser != nil {
errBody = b.ReadCloser.Close()
}
b.err = errors.Join(errBody, errConnection)
})
return b.err
}
func newUtlsRoundTripper(proxyURL string) *utlsRoundTripper {
var dialer proxy.Dialer = proxy.Direct
if proxyURL != "" {
proxyDialer, mode, errBuild := proxyutil.BuildDialer(proxyURL)
if errBuild != nil {
log.Errorf("utls: failed to configure proxy dialer for %q: %v", proxyutil.Redact(proxyURL), errBuild)
} else if mode != proxyutil.ModeInherit && proxyDialer != nil {
dialer = proxyDialer
}
}
return &utlsRoundTripper{dialer: dialer}
}
func (t *utlsRoundTripper) createConnection(ctx context.Context, host, addr string) (*http2.ClientConn, error) {
contextDialer, ok := t.dialer.(proxy.ContextDialer)
if !ok {
return nil, fmt.Errorf("utls: dialer does not support context cancellation")
}
conn, errDial := contextDialer.DialContext(ctx, "tcp", addr)
if errDial != nil {
return nil, fmt.Errorf("utls: dial upstream: %w", errDial)
}
tlsConfig := &tls.Config{ServerName: host}
tlsConn := tls.UClient(conn, tlsConfig, tls.HelloChrome_Auto)
if errHandshake := tlsConn.HandshakeContext(ctx); errHandshake != nil {
if errors.Is(errHandshake, context.Canceled) || errors.Is(errHandshake, context.DeadlineExceeded) {
return nil, fmt.Errorf("utls: TLS handshake: %w", errHandshake)
}
if errClose := conn.Close(); errClose != nil {
return nil, fmt.Errorf("utls: TLS handshake: %w; close connection: %v", errHandshake, errClose)
}
return nil, fmt.Errorf("utls: TLS handshake: %w", errHandshake)
}
tr := &http2.Transport{}
h2Conn, errClientConn := tr.NewClientConn(tlsConn)
if errClientConn != nil {
if errClose := tlsConn.Close(); errClose != nil {
return nil, fmt.Errorf("utls: initialize HTTP/2 connection: %w; close TLS connection: %v", errClientConn, errClose)
}
return nil, fmt.Errorf("utls: initialize HTTP/2 connection: %w", errClientConn)
}
return h2Conn, nil
}
func (t *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
hostname := req.URL.Hostname()
port := req.URL.Port()
if port == "" {
port = "443"
}
addr := net.JoinHostPort(hostname, port)
h2Conn, err := t.createConnection(req.Context(), hostname, addr)
if err != nil {
return nil, err
}
resp, err := h2Conn.RoundTrip(req)
if err != nil {
if errClose := h2Conn.Close(); errClose != nil {
log.Debugf("utls: close connection after round trip failure: %v", errClose)
}
return nil, err
}
if resp == nil {
if errClose := h2Conn.Close(); errClose != nil {
log.Debugf("utls: close connection after empty response: %v", errClose)
}
return nil, fmt.Errorf("utls: upstream returned an empty response")
}
if resp.Body == nil {
resp.Body = http.NoBody
}
resp.Body = &closeConnectionBody{
ReadCloser: resp.Body,
closeConnection: h2Conn.Close,
}
return resp, nil
}
// claudeCodeSessionCacheCapacity bounds the per-transport TLS session cache for
// the Anthropic inference plane.
const claudeCodeSessionCacheCapacity = 32
// newClaudeCodeTLSConfig builds the uTLS config for one inference-plane dial.
//
// OmitEmptyPsk keeps the pre_shared_key extension silent until a session is
// cached, so an unresumed ClientHello stays byte-identical to the captured
// native handshake. PreferSkipResumptionOnNilExtension turns uTLS's HelloCustom
// "resume without the matching extension" panic into a skipped resumption.
func newClaudeCodeTLSConfig(host string, sessionCache tls.ClientSessionCache) *tls.Config {
return &tls.Config{
ServerName: host,
ClientSessionCache: sessionCache,
OmitEmptyPsk: true,
PreferSkipResumptionOnNilExtension: true,
}
}
// claudeCodeTLSClientHelloSpec reproduces the deterministic Node/OpenSSL
// ClientHello emitted by Claude Code 2.1.220 on macOS arm64. Keep this spec in
// sync with a fresh native capture whenever the advertised Claude Code version
// changes.
func claudeCodeTLSClientHelloSpec() *tls.ClientHelloSpec {
return &tls.ClientHelloSpec{
CipherSuites: []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_128_CBC_SHA,
tls.TLS_RSA_WITH_AES_256_CBC_SHA,
},
CompressionMethods: []uint8{0},
Extensions: []tls.TLSExtension{
&tls.SNIExtension{},
&tls.ExtendedMasterSecretExtension{},
&tls.RenegotiationInfoExtension{Renegotiation: tls.RenegotiateOnceAsClient},
&tls.SupportedCurvesExtension{Curves: []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384}},
&tls.SupportedPointsExtension{SupportedPoints: []byte{0}},
&tls.SessionTicketExtension{},
&tls.ALPNExtension{AlpnProtocols: []string{"http/1.1"}},
&tls.StatusRequestExtension{},
&tls.SignatureAlgorithmsExtension{SupportedSignatureAlgorithms: []tls.SignatureScheme{
tls.ECDSAWithP256AndSHA256,
tls.PSSWithSHA256,
tls.PKCS1WithSHA256,
tls.ECDSAWithP384AndSHA384,
tls.PSSWithSHA384,
tls.PKCS1WithSHA384,
tls.PSSWithSHA512,
tls.PKCS1WithSHA512,
tls.PKCS1WithSHA1,
}},
&tls.SCTExtension{},
&tls.KeyShareExtension{KeyShares: []tls.KeyShare{{Group: tls.X25519}}},
&tls.PSKKeyExchangeModesExtension{Modes: []uint8{tls.PskModeDHE}},
&tls.SupportedVersionsExtension{Versions: []uint16{tls.VersionTLS13, tls.VersionTLS12}},
&tls.UtlsPaddingExtension{GetPaddingLen: tls.BoringPaddingStyle},
// pre_shared_key MUST be the final extension (RFC 8446 4.2.11), after
// padding. It contributes zero bytes until a cached session exists.
&tls.UtlsPreSharedKeyExtension{},
},
}
}
const claudeCodeRoundTripperCacheCapacity = 64
var claudeCodeRoundTripperCache = internalcache.NewBoundedLRU[string, http.RoundTripper](
claudeCodeRoundTripperCacheCapacity,
func(_ string, roundTripper http.RoundTripper) {
if transport, ok := roundTripper.(interface{ CloseIdleConnections() }); ok {
transport.CloseIdleConnections()
}
},
)
var claudeCodeMessagesHeaderOrder = []string{
"Accept",
"Authorization",
"Content-Type",
"User-Agent",
"X-Claude-Code-Session-Id",
"X-Stainless-Arch",
"X-Stainless-Lang",
"X-Stainless-OS",
"X-Stainless-Package-Version",
"X-Stainless-Retry-Count",
"X-Stainless-Runtime",
"X-Stainless-Runtime-Version",
"X-Stainless-Timeout",
"anthropic-beta",
"anthropic-dangerous-direct-browser-access",
"anthropic-version",
"x-app",
"x-client-request-id",
"Connection",
"Host",
"Accept-Encoding",
"Content-Length",
}
var claudeCodeCountTokensHeaderOrder = []string{
"Accept",
"Authorization",
"Content-Type",
"User-Agent",
"X-Claude-Code-Session-Id",
"X-Stainless-Arch",
"X-Stainless-Lang",
"X-Stainless-OS",
"X-Stainless-Package-Version",
"X-Stainless-Retry-Count",
"X-Stainless-Runtime",
"X-Stainless-Runtime-Version",
"anthropic-beta",
"anthropic-dangerous-direct-browser-access",
"anthropic-version",
"x-app",
"x-client-request-id",
"Connection",
"Host",
"Accept-Encoding",
"Content-Length",
}
func claudeCodeRequestHeaderOrder(_, requestTarget string) []string {
if strings.HasPrefix(requestTarget, "/v1/messages/count_tokens") {
return claudeCodeCountTokensHeaderOrder
}
return claudeCodeMessagesHeaderOrder
}
func cachedClaudeCodeRoundTripper(proxyURL string) http.RoundTripper {
return claudeCodeRoundTripperCache.GetOrAdd(proxyURL, func() http.RoundTripper {
return newClaudeCodeRoundTripper(proxyURL)
})
}
func newClaudeCodeRoundTripper(proxyURL string) http.RoundTripper {
// The cache is scoped to this round tripper, which is already keyed by proxy,
// so resumption never crosses proxy boundaries.
sessionCache := tls.NewLRUClientSessionCache(claudeCodeSessionCacheCapacity)
var dialer proxy.Dialer = proxy.Direct
if proxyURL != "" {
proxyDialer, mode, errBuild := proxyutil.BuildDialer(proxyURL)
if errBuild != nil {
log.Errorf("claude tls: failed to configure proxy dialer for %q: %v", proxyutil.Redact(proxyURL), errBuild)
} else if mode != proxyutil.ModeInherit && proxyDialer != nil {
dialer = proxyDialer
}
}
transport := &http.Transport{
ForceAttemptHTTP2: false,
DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
var (
conn net.Conn
err error
)
if contextDialer, ok := dialer.(proxy.ContextDialer); ok {
conn, err = contextDialer.DialContext(ctx, network, addr)
} else {
conn, err = dialer.Dial(network, addr)
}
if err != nil {
return nil, fmt.Errorf("claude tls: dial upstream: %w", err)
}
host, _, errSplit := net.SplitHostPort(addr)
if errSplit != nil {
if errClose := conn.Close(); errClose != nil {
log.Debugf("claude tls: close failed connection: %v", errClose)
}
return nil, fmt.Errorf("claude tls: split upstream address: %w", errSplit)
}
tlsConn := tls.UClient(conn, newClaudeCodeTLSConfig(host, sessionCache), tls.HelloCustom)
if errPreset := tlsConn.ApplyPreset(claudeCodeTLSClientHelloSpec()); errPreset != nil {
if errClose := tlsConn.Close(); errClose != nil {
log.Debugf("claude tls: close connection after preset failure: %v", errClose)
}
return nil, fmt.Errorf("claude tls: apply Claude Code ClientHello: %w", errPreset)
}
if errHandshake := tlsConn.HandshakeContext(ctx); errHandshake != nil {
if errClose := tlsConn.Close(); errClose != nil {
log.Debugf("claude tls: close connection after handshake failure: %v", errClose)
}
return nil, fmt.Errorf("claude tls: handshake upstream: %w", errHandshake)
}
return httpwire.NewOrderedRequestConn(tlsConn, claudeCodeRequestHeaderOrder), nil
},
}
return transport
}
// fallbackRoundTripper uses provider-specific TLS fingerprints for protected
// HTTPS hosts and falls back to the standard transport for all other requests.
type fallbackRoundTripper struct {
anthropic http.RoundTripper
chrome http.RoundTripper
fallback http.RoundTripper
}
func (f *fallbackRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if IsAnthropicUpstreamURL(req.URL) {
return f.anthropic.RoundTrip(req)
}
if req.URL.Scheme == "https" && strings.EqualFold(req.URL.Hostname(), "chatgpt.com") {
return f.chrome.RoundTrip(req)
}
return f.fallback.RoundTrip(req)
}
// NewUtlsHTTPClient creates an HTTP client using provider-specific TLS
// fingerprints for protected hosts. It uses Claude Code's Node/OpenSSL profile
// for Anthropic and a Chrome profile for ChatGPT, with a standard-transport
// fallback for other hosts.
func NewUtlsHTTPClient(ctx context.Context, cfg *config.Config, auth *cliproxyauth.Auth, timeout time.Duration) *http.Client {
var proxyURL string
if auth != nil {
proxyURL = strings.TrimSpace(auth.ProxyURL)
}
if proxyURL == "" && cfg != nil {
proxyURL = strings.TrimSpace(cfg.ProxyURL)
}
var ctxRoundTripper http.RoundTripper
if ctx != nil {
ctxRoundTripper, _ = ctx.Value("cliproxy.roundtripper").(http.RoundTripper)
}
var chromeRT http.RoundTripper = newUtlsRoundTripper(proxyURL)
var anthropicRT http.RoundTripper = cachedClaudeCodeRoundTripper(proxyURL)
var standardTransport http.RoundTripper = http.DefaultTransport
if proxyURL != "" {
if transport := buildProxyTransport(proxyURL); transport != nil {
standardTransport = transport
}
} else if ctxRoundTripper != nil {
chromeRT = ctxRoundTripper
anthropicRT = ctxRoundTripper
standardTransport = ctxRoundTripper
}
client := &http.Client{
Transport: &fallbackRoundTripper{
anthropic: anthropicRT,
chrome: chromeRT,
fallback: standardTransport,
},
}
if timeout > 0 {
client.Timeout = timeout
}
return client
}