From 4ac7290e1fe4558bb51287b6f2f1f2efb3cd5022 Mon Sep 17 00:00:00 2001 From: kyx236 Date: Fri, 6 Feb 2026 17:44:29 +0800 Subject: [PATCH] feat: add a new TLS client with browser fingerprinting and implement Codex API authentication. --- backend/internal/auth/codex_api.go | 223 ++++++++++------------------- backend/internal/client/tls.go | 9 ++ 2 files changed, 84 insertions(+), 148 deletions(-) diff --git a/backend/internal/auth/codex_api.go b/backend/internal/auth/codex_api.go index 0b93333..de3893d 100644 --- a/backend/internal/auth/codex_api.go +++ b/backend/internal/auth/codex_api.go @@ -2,24 +2,18 @@ package auth import ( "bytes" - "context" - "crypto/tls" "encoding/base64" "encoding/json" "fmt" "io" "math/rand" - "net" "net/http" - "net/http/cookiejar" "net/url" "strings" "time" + "codex-pool/internal/client" "codex-pool/internal/mail" - - utls "github.com/refraction-networking/utls" - "golang.org/x/net/http2" ) func init() { @@ -36,7 +30,7 @@ var UseFixedAuthURL = false // CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现 type CodexAPIAuth struct { - client *http.Client + tlsClient *client.TLSClient email string password string workspaceID string @@ -47,140 +41,41 @@ type CodexAPIAuth struct { sentinelToken string solvedPow string userAgent string + secChUa string + secChPlatform string proxyURL string logger *AuthLogger } -// utlsRoundTripper - 模拟 Chrome TLS 指纹 -type utlsRoundTripper struct { - proxyURL *url.URL -} - -func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { - var conn net.Conn - var err error - - targetHost := req.URL.Host - if !strings.Contains(targetHost, ":") { - if req.URL.Scheme == "https" { - targetHost += ":443" - } else { - targetHost += ":80" - } - } - - // 通过代理连接 - if rt.proxyURL != nil { - proxyHost := rt.proxyURL.Host - conn, err = net.DialTimeout("tcp", proxyHost, 30*time.Second) - if err != nil { - return nil, fmt.Errorf("proxy dial error: %w", err) - } - - // 构建 CONNECT 请求,支持代理认证 - connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n", targetHost, targetHost) - - // 添加代理认证头 - if rt.proxyURL.User != nil { - username := rt.proxyURL.User.Username() - password, _ := rt.proxyURL.User.Password() - auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) - connectReq += fmt.Sprintf("Proxy-Authorization: Basic %s\r\n", auth) - } - connectReq += "\r\n" - - _, err = conn.Write([]byte(connectReq)) - if err != nil { - conn.Close() - return nil, fmt.Errorf("proxy connect write error: %w", err) - } - - // 读取代理响应 - buf := make([]byte, 4096) - n, err := conn.Read(buf) - if err != nil { - conn.Close() - return nil, fmt.Errorf("proxy connect read error: %w", err) - } - response := string(buf[:n]) - if !strings.Contains(response, "200") { - conn.Close() - return nil, fmt.Errorf("proxy connect failed: %s", response) - } - } else { - conn, err = net.DialTimeout("tcp", targetHost, 30*time.Second) - if err != nil { - return nil, fmt.Errorf("dial error: %w", err) - } - } - - // 使用 uTLS 进行 TLS 握手,模拟 Chrome - tlsConn := utls.UClient(conn, &utls.Config{ - ServerName: req.URL.Hostname(), - InsecureSkipVerify: true, - }, utls.HelloChrome_Auto) - - err = tlsConn.Handshake() - if err != nil { - conn.Close() - return nil, fmt.Errorf("tls handshake error: %w", err) - } - - // 使用 HTTP/2 或 HTTP/1.1 - alpn := tlsConn.ConnectionState().NegotiatedProtocol - if alpn == "h2" { - // HTTP/2 - t2 := &http2.Transport{ - DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { - return tlsConn, nil - }, - } - return t2.RoundTrip(req) - } - - // HTTP/1.1 - t1 := &http.Transport{ - DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - return tlsConn, nil - }, - } - return t1.RoundTrip(req) -} - -// NewCodexAPIAuth 创建 CodexAuth 实例 +// NewCodexAPIAuth 创建 CodexAuth 实例(使用随机 TLS 指纹) // authURL 和 sessionID 由 S2A 生成 func NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) { - var proxyURL *url.URL - if proxy != "" { - var err error - proxyURL, err = url.Parse(proxy) - if err != nil { - return nil, fmt.Errorf("解析代理地址失败: %v", err) - } + tlsClient, err := client.New(proxy) + if err != nil { + return nil, fmt.Errorf("创建 TLS 客户端失败: %v", err) } - jar, _ := cookiejar.New(nil) - client := &http.Client{ - Transport: &utlsRoundTripper{proxyURL: proxyURL}, - Jar: jar, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse // 禁用自动重定向,手动跟随 - }, - Timeout: 30 * time.Second, + fpInfo := tlsClient.GetFingerprintInfo() + ua, secChUa, secChPlatform := tlsClient.GetHeadersInfo() + + if logger != nil { + logger.LogStep(StepBrowserStart, "指纹: %s", fpInfo) } return &CodexAPIAuth{ - client: client, - email: email, - password: password, - workspaceID: workspaceID, - authURL: authURL, - sessionID: sessionID, - deviceID: generateUUID(), - sid: generateUUID(), - userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36", - proxyURL: proxy, - logger: logger, + tlsClient: tlsClient, + email: email, + password: password, + workspaceID: workspaceID, + authURL: authURL, + sessionID: sessionID, + deviceID: generateUUID(), + sid: generateUUID(), + userAgent: ua, + secChUa: secChUa, + secChPlatform: secChPlatform, + proxyURL: proxy, + logger: logger, }, nil } @@ -278,7 +173,7 @@ func (c *CodexAPIAuth) getRequirementsToken() string { return "gAAAAAC" + encoded + "~S" } -// doRequest 执行 HTTP 请求 +// doRequest 执行 HTTP 请求(通过 TLSClient 随机指纹) func (c *CodexAPIAuth) doRequest(method, urlStr string, body interface{}, headers map[string]string) (*http.Response, []byte, error) { var bodyReader io.Reader if body != nil { @@ -291,13 +186,17 @@ func (c *CodexAPIAuth) doRequest(method, urlStr string, body interface{}, header return nil, nil, err } - // 设置默认头 + // 设置默认头(使用指纹动态生成的值) req.Header.Set("User-Agent", c.userAgent) req.Header.Set("Accept", "*/*") req.Header.Set("Accept-Language", "en-US,en;q=0.9") - req.Header.Set("sec-ch-ua", `"Not(A:Brand";v="8", "Chromium";v="110", "Google Chrome";v="110"`) + if c.secChUa != "" { + req.Header.Set("sec-ch-ua", c.secChUa) + } req.Header.Set("sec-ch-ua-mobile", "?0") - req.Header.Set("sec-ch-ua-platform", `"Windows"`) + if c.secChPlatform != "" { + req.Header.Set("sec-ch-ua-platform", c.secChPlatform) + } req.Header.Set("sec-fetch-dest", "empty") req.Header.Set("sec-fetch-mode", "cors") req.Header.Set("sec-fetch-site", "same-origin") @@ -306,7 +205,7 @@ func (c *CodexAPIAuth) doRequest(method, urlStr string, body interface{}, header req.Header.Set(k, v) } - resp, err := c.client.Do(req) + resp, err := c.tlsClient.Do(req) if err != nil { return nil, nil, err } @@ -744,7 +643,7 @@ func min(a, b int) int { return b } -// CompleteWithCodexAPI 使用纯 API 方式完成授权 +// CompleteWithCodexAPI 使用纯 API 方式完成授权(带 403 重试换指纹机制) // authURL 和 sessionID 由 S2A 生成 func CompleteWithCodexAPI(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (string, error) { if logger != nil { @@ -755,18 +654,46 @@ func CompleteWithCodexAPI(email, password, workspaceID, authURL, sessionID, prox } } - auth, err := NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy, logger) - if err != nil { - if logger != nil { - logger.LogError(StepBrowserStart, "创建 CodexAuth 失败: %v", err) + // 403 重试机制 - 最多重试 3 次,每次换新指纹 + var lastErr error + for retry := 0; retry < 3; retry++ { + auth, err := NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy, logger) + if err != nil { + lastErr = err + if logger != nil { + logger.LogError(StepBrowserStart, "创建 CodexAuth 失败: %v", err) + } + if retry < 2 { + if logger != nil { + logger.LogStep(StepBrowserStart, "重试 %d/3,换新指纹...", retry+1) + } + continue + } + return "", fmt.Errorf("创建 CodexAuth 失败 (已重试3次): %v", err) } - return "", err + + code, err := auth.ObtainAuthorizationCode() + if err != nil { + auth.tlsClient.Close() + // 检查是否为 403 错误 + if strings.Contains(err.Error(), "403") { + lastErr = err + if retry < 2 { + if logger != nil { + logger.LogStep(StepBrowserStart, "遇到 403,重试 %d/3,换新指纹...", retry+1) + } + time.Sleep(time.Duration(1+retry) * time.Second) // 递增延迟 + continue + } + return "", fmt.Errorf("授权失败: %v (403 已重试3次)", err) + } + // 非 403 错误,不重试 + return "", err + } + + auth.tlsClient.Close() + return code, nil } - code, err := auth.ObtainAuthorizationCode() - if err != nil { - return "", err - } - - return code, nil + return "", fmt.Errorf("授权失败 (已重试3次): %v", lastErr) } diff --git a/backend/internal/client/tls.go b/backend/internal/client/tls.go index fa93816..3087540 100644 --- a/backend/internal/client/tls.go +++ b/backend/internal/client/tls.go @@ -588,6 +588,15 @@ func (c *TLSClient) GetFingerprintInfo() string { return fmt.Sprintf("%s/%s %s (%s)", typeName, fp.Browser, fp.Version, fp.Platform) } +// GetHeadersInfo 获取指纹对应的 User-Agent、sec-ch-ua、sec-ch-ua-platform +// 供 codex_api 等外部模块使用,确保 headers 与 TLS 指纹一致 +func (c *TLSClient) GetHeadersInfo() (userAgent, secChUa, secChPlatform string) { + userAgent = c.userAgent + secChUa = c.generateSecChUa() + secChPlatform = c.getPlatformHeader() + return +} + // Close 关闭客户端 func (c *TLSClient) Close() { if c.azureSession != nil {