package helps import ( "net" "net/http" "strings" "sync" "time" tls "github.com/refraction-networking/utls" "github.com/router-for-me/CLIProxyAPI/v7/internal/config" 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 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", proxyutil.Redact(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}
package helps
import (
"net"
"net/http"
"strings"
"sync"
"time"
tls "github.com/refraction-networking/utls"
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
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 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", proxyutil.Redact(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
}