// Package transport provides a shared uTLS HTTP/2 round-tripper with Chrome // TLS fingerprinting and per-host connection pooling. Used by both the upstream // proxy client and the OAuth token refresh client. package transport import ( "net" "net/http" "sync" "time" tls "github.com/refraction-networking/utls" "golang.org/x/net/http2" ) // UTLS implements http.RoundTripper using uTLS (Chrome fingerprint) over HTTP/2. // It maintains a per-host connection pool with coordination for concurrent // requests to the same host. type UTLS struct { mu sync.Mutex connections map[string]*http2.ClientConn pending map[string]*sync.Cond dialTimeout time.Duration } // NewUTLS creates a uTLS HTTP/2 round-tripper with a 10-second dial timeout. func NewUTLS() *UTLS { return &UTLS{ connections: make(map[string]*http2.ClientConn), pending: make(map[string]*sync.Cond), dialTimeout: 10 * time.Second, } } // NewHTTPClient returns an http.Client using uTLS transport with the given // request timeout. Pass 0 for no timeout (streaming). func NewHTTPClient(timeout time.Duration) *http.Client { return &http.Client{ Timeout: timeout, Transport: NewUTLS(), } } func (t *UTLS) 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 *UTLS) createConnection(host, addr string) (*http2.ClientConn, error) { conn, err := net.DialTimeout("tcp", addr, t.dialTimeout) 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 } // RoundTrip implements http.RoundTripper with uTLS Chrome fingerprinting. func (t *UTLS) 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 }