From 6b6d030ed3fa27e30ef35a0d500d4f48d5ed85d4 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Fri, 30 Jan 2026 21:29:41 +0800 Subject: [PATCH] feat(auth): add custom HTTP client with utls for Claude API authentication Introduce a custom HTTP client utilizing utls with Firefox TLS fingerprinting to bypass Cloudflare fingerprinting on Anthropic domains. Includes support for proxy configuration and enhanced connection management for HTTP/2. --- go.mod | 1 + go.sum | 2 + internal/auth/claude/anthropic_auth.go | 8 +- internal/auth/claude/utls_transport.go | 165 +++++++++++++++++++++++++ sdk/auth/claude.go | 3 + 5 files changed, 176 insertions(+), 3 deletions(-) create mode 100644 internal/auth/claude/utls_transport.go diff --git a/go.mod b/go.mod index 963d9c49..32080fd7 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/klauspost/compress v1.17.4 github.com/minio/minio-go/v7 v7.0.66 + github.com/refraction-networking/utls v1.8.2 github.com/sirupsen/logrus v1.9.3 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/tidwall/gjson v1.18.0 diff --git a/go.sum b/go.sum index 4705336b..b57b919a 100644 --- a/go.sum +++ b/go.sum @@ -118,6 +118,8 @@ github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0= github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/refraction-networking/utls v1.8.2 h1:j4Q1gJj0xngdeH+Ox/qND11aEfhpgoEvV+S9iJ2IdQo= +github.com/refraction-networking/utls v1.8.2/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= diff --git a/internal/auth/claude/anthropic_auth.go b/internal/auth/claude/anthropic_auth.go index 54edce3b..e0f6e3c8 100644 --- a/internal/auth/claude/anthropic_auth.go +++ b/internal/auth/claude/anthropic_auth.go @@ -14,7 +14,6 @@ import ( "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" - "github.com/router-for-me/CLIProxyAPI/v6/internal/util" log "github.com/sirupsen/logrus" ) @@ -51,7 +50,8 @@ type ClaudeAuth struct { } // NewClaudeAuth creates a new Anthropic authentication service. -// It initializes the HTTP client with proxy settings from the configuration. +// It initializes the HTTP client with a custom TLS transport that uses Firefox +// fingerprint to bypass Cloudflare's TLS fingerprinting on Anthropic domains. // // Parameters: // - cfg: The application configuration containing proxy settings @@ -59,8 +59,10 @@ type ClaudeAuth struct { // Returns: // - *ClaudeAuth: A new Claude authentication service instance func NewClaudeAuth(cfg *config.Config) *ClaudeAuth { + // Use custom HTTP client with Firefox TLS fingerprint to bypass + // Cloudflare's bot detection on Anthropic domains return &ClaudeAuth{ - httpClient: util.SetProxy(&cfg.SDKConfig, &http.Client{}), + httpClient: NewAnthropicHttpClient(&cfg.SDKConfig), } } diff --git a/internal/auth/claude/utls_transport.go b/internal/auth/claude/utls_transport.go new file mode 100644 index 00000000..2cb840b2 --- /dev/null +++ b/internal/auth/claude/utls_transport.go @@ -0,0 +1,165 @@ +// Package claude provides authentication functionality for Anthropic's Claude API. +// This file implements a custom HTTP transport using utls to bypass TLS fingerprinting. +package claude + +import ( + "net/http" + "net/url" + "strings" + "sync" + + tls "github.com/refraction-networking/utls" + "github.com/router-for-me/CLIProxyAPI/v6/sdk/config" + log "github.com/sirupsen/logrus" + "golang.org/x/net/http2" + "golang.org/x/net/proxy" +) + +// utlsRoundTripper implements http.RoundTripper using utls with Firefox fingerprint +// to bypass Cloudflare's TLS fingerprinting on Anthropic domains. +type utlsRoundTripper struct { + // mu protects the connections map and pending map + mu sync.Mutex + // connections caches HTTP/2 client connections per host + connections map[string]*http2.ClientConn + // pending tracks hosts that are currently being connected to (prevents race condition) + pending map[string]*sync.Cond + // dialer is used to create network connections, supporting proxies + dialer proxy.Dialer +} + +// newUtlsRoundTripper creates a new utls-based round tripper with optional proxy support +func newUtlsRoundTripper(cfg *config.SDKConfig) *utlsRoundTripper { + var dialer proxy.Dialer = proxy.Direct + if cfg != nil && cfg.ProxyURL != "" { + proxyURL, err := url.Parse(cfg.ProxyURL) + if err != nil { + log.Errorf("failed to parse proxy URL %q: %v", cfg.ProxyURL, err) + } else { + pDialer, err := proxy.FromURL(proxyURL, proxy.Direct) + if err != nil { + log.Errorf("failed to create proxy dialer for %q: %v", cfg.ProxyURL, err) + } else { + dialer = pDialer + } + } + } + + return &utlsRoundTripper{ + connections: make(map[string]*http2.ClientConn), + pending: make(map[string]*sync.Cond), + dialer: dialer, + } +} + +// getOrCreateConnection gets an existing connection or creates a new one. +// It uses a per-host locking mechanism to prevent multiple goroutines from +// creating connections to the same host simultaneously. +func (t *utlsRoundTripper) getOrCreateConnection(host, addr string) (*http2.ClientConn, error) { + t.mu.Lock() + + // Check if connection exists and is usable + if h2Conn, ok := t.connections[host]; ok && h2Conn.CanTakeNewRequest() { + t.mu.Unlock() + return h2Conn, nil + } + + // Check if another goroutine is already creating a connection + if cond, ok := t.pending[host]; ok { + // Wait for the other goroutine to finish + cond.Wait() + // Check if connection is now available + if h2Conn, ok := t.connections[host]; ok && h2Conn.CanTakeNewRequest() { + t.mu.Unlock() + return h2Conn, nil + } + // Connection still not available, we'll create one + } + + // Mark this host as pending + cond := sync.NewCond(&t.mu) + t.pending[host] = cond + t.mu.Unlock() + + // Create connection outside the lock + h2Conn, err := t.createConnection(host, addr) + + t.mu.Lock() + defer t.mu.Unlock() + + // Remove pending marker and wake up waiting goroutines + delete(t.pending, host) + cond.Broadcast() + + if err != nil { + return nil, err + } + + // Store the new connection + t.connections[host] = h2Conn + return h2Conn, nil +} + +// createConnection creates a new HTTP/2 connection with Firefox TLS fingerprint +func (t *utlsRoundTripper) createConnection(host, addr string) (*http2.ClientConn, error) { + conn, err := t.dialer.Dial("tcp", addr) + if err != nil { + return nil, err + } + + tlsConfig := &tls.Config{ServerName: host} + tlsConn := tls.UClient(conn, tlsConfig, tls.HelloFirefox_Auto) + + if err := tlsConn.Handshake(); err != nil { + conn.Close() + return nil, err + } + + tr := &http2.Transport{} + h2Conn, err := tr.NewClientConn(tlsConn) + if err != nil { + tlsConn.Close() + return nil, err + } + + return h2Conn, nil +} + +// RoundTrip implements http.RoundTripper +func (t *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { + host := req.URL.Host + addr := host + if !strings.Contains(addr, ":") { + addr += ":443" + } + + // Get hostname without port for TLS ServerName + hostname := req.URL.Hostname() + + h2Conn, err := t.getOrCreateConnection(hostname, addr) + if err != nil { + return nil, err + } + + resp, err := h2Conn.RoundTrip(req) + if err != nil { + // Connection failed, remove it from cache + t.mu.Lock() + if cached, ok := t.connections[hostname]; ok && cached == h2Conn { + delete(t.connections, hostname) + } + t.mu.Unlock() + return nil, err + } + + return resp, nil +} + +// NewAnthropicHttpClient creates an HTTP client that bypasses TLS fingerprinting +// for Anthropic domains by using utls with Firefox fingerprint. +// It accepts optional SDK configuration for proxy settings. +func NewAnthropicHttpClient(cfg *config.SDKConfig) *http.Client { + return &http.Client{ + Transport: newUtlsRoundTripper(cfg), + } +} diff --git a/sdk/auth/claude.go b/sdk/auth/claude.go index 2c7a8988..a6b19af5 100644 --- a/sdk/auth/claude.go +++ b/sdk/auth/claude.go @@ -176,13 +176,16 @@ waitForCallback: } if result.State != state { + log.Errorf("State mismatch: expected %s, got %s", state, result.State) return nil, claude.NewAuthenticationError(claude.ErrInvalidState, fmt.Errorf("state mismatch")) } log.Debug("Claude authorization code received; exchanging for tokens") + log.Debugf("Code: %s, State: %s", result.Code[:min(20, len(result.Code))], state) authBundle, err := authSvc.ExchangeCodeForTokens(ctx, result.Code, state, pkceCodes) if err != nil { + log.Errorf("Token exchange failed: %v", err) return nil, claude.NewAuthenticationError(claude.ErrCodeExchangeFailed, err) }