0df28e9dd8
- Unify duplicate uTLS transports into shared internal/transport package - Extract shared version constant into internal/version - Move LoadDefaultCredentials from config to auth (remove config→auth import) - Deduplicate handler.go: extract telemetry/error helpers (324→268 lines) - Break up main.go::run() into initCredential/initEmbedded - Eliminate logging.Config duplication (use config.LoggingConfig directly) - Extract logWriter to embedded/log.go, SSE fixtures to consts in sniff.go - Use uTLS client for usage polling (consistent TLS fingerprint) - Handle sjson.SetBytes errors in sanitize.go instead of silently swallowing - Document reverse-engineered magic values in billing.go - Unexport Credential.CooldownUntil (internal state) - Replace hardcoded auth bypass paths with map in server.go
130 lines
2.9 KiB
Go
130 lines
2.9 KiB
Go
// 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
|
|
}
|