407 lines · 13.1 KB · Go
RawDownload
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}