Spaces:
Paused
Paused
| package helps | |
| import ( | |
| "net" | |
| "net/http" | |
| "strings" | |
| "sync" | |
| "time" | |
| tls "github.com/refraction-networking/utls" | |
| "github.com/router-for-me/CLIProxyAPI/v6/internal/config" | |
| cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" | |
| "github.com/router-for-me/CLIProxyAPI/v6/sdk/proxyutil" | |
| log "github.com/sirupsen/logrus" | |
| "golang.org/x/net/http2" | |
| "golang.org/x/net/proxy" | |
| ) | |
| // utlsRoundTripper implements http.RoundTripper using utls with Chrome fingerprint | |
| // to bypass Cloudflare's TLS fingerprinting on Anthropic domains. | |
| type utlsRoundTripper struct { | |
| mu sync.Mutex | |
| connections map[string]*http2.ClientConn | |
| pending map[string]*sync.Cond | |
| dialer proxy.Dialer | |
| } | |
| 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", proxyURL, errBuild) | |
| } else if mode != proxyutil.ModeInherit && proxyDialer != nil { | |
| dialer = proxyDialer | |
| } | |
| } | |
| return &utlsRoundTripper{ | |
| connections: make(map[string]*http2.ClientConn), | |
| pending: make(map[string]*sync.Cond), | |
| dialer: dialer, | |
| } | |
| } | |
| func (t *utlsRoundTripper) getOrCreateConnection(host, addr string) (*http2.ClientConn, error) { | |
| t.mu.Lock() | |
| if h2Conn, ok := t.connections[host]; ok && h2Conn.CanTakeNewRequest() { | |
| t.mu.Unlock() | |
| return h2Conn, nil | |
| } | |
| if cond, ok := t.pending[host]; ok { | |
| cond.Wait() | |
| if h2Conn, ok := t.connections[host]; ok && h2Conn.CanTakeNewRequest() { | |
| t.mu.Unlock() | |
| return h2Conn, nil | |
| } | |
| } | |
| cond := sync.NewCond(&t.mu) | |
| t.pending[host] = cond | |
| t.mu.Unlock() | |
| h2Conn, err := t.createConnection(host, addr) | |
| t.mu.Lock() | |
| defer t.mu.Unlock() | |
| delete(t.pending, host) | |
| cond.Broadcast() | |
| if err != nil { | |
| return nil, err | |
| } | |
| t.connections[host] = h2Conn | |
| return h2Conn, nil | |
| } | |
| 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.HelloChrome_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 | |
| } | |
| 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.getOrCreateConnection(hostname, addr) | |
| if err != nil { | |
| return nil, err | |
| } | |
| resp, err := h2Conn.RoundTrip(req) | |
| if err != nil { | |
| 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 | |
| } | |
| // anthropicHosts contains the hosts that should use utls Chrome TLS fingerprint. | |
| var anthropicHosts = map[string]struct{}{ | |
| "api.anthropic.com": {}, | |
| } | |
| // fallbackRoundTripper uses utls for Anthropic HTTPS hosts and falls back to | |
| // standard transport for all other requests (non-HTTPS or non-Anthropic hosts). | |
| type fallbackRoundTripper struct { | |
| utls *utlsRoundTripper | |
| fallback http.RoundTripper | |
| } | |
| func (f *fallbackRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { | |
| if req.URL.Scheme == "https" { | |
| if _, ok := anthropicHosts[strings.ToLower(req.URL.Hostname())]; ok { | |
| return f.utls.RoundTrip(req) | |
| } | |
| } | |
| return f.fallback.RoundTrip(req) | |
| } | |
| // NewUtlsHTTPClient creates an HTTP client using utls Chrome TLS fingerprint. | |
| // Use this for Claude API requests to match real Claude Code's TLS behavior. | |
| // Falls back to standard transport for non-HTTPS requests. | |
| func NewUtlsHTTPClient(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) | |
| } | |
| utlsRT := newUtlsRoundTripper(proxyURL) | |
| var standardTransport http.RoundTripper = &http.Transport{ | |
| DialContext: (&net.Dialer{ | |
| Timeout: 30 * time.Second, | |
| KeepAlive: 30 * time.Second, | |
| }).DialContext, | |
| } | |
| if proxyURL != "" { | |
| if transport := buildProxyTransport(proxyURL); transport != nil { | |
| standardTransport = transport | |
| } | |
| } | |
| client := &http.Client{ | |
| Transport: &fallbackRoundTripper{ | |
| utls: utlsRT, | |
| fallback: standardTransport, | |
| }, | |
| } | |
| if timeout > 0 { | |
| client.Timeout = timeout | |
| } | |
| return client | |
| } | |