feat: add a new TLS client with browser fingerprinting and implement Codex API authentication.

This commit is contained in:
2026-02-06 17:44:29 +08:00
parent 1c1bdc3152
commit 4ac7290e1f
2 changed files with 84 additions and 148 deletions

View File

@@ -2,24 +2,18 @@ package auth
import ( import (
"bytes" "bytes"
"context"
"crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"math/rand" "math/rand"
"net"
"net/http" "net/http"
"net/http/cookiejar"
"net/url" "net/url"
"strings" "strings"
"time" "time"
"codex-pool/internal/client"
"codex-pool/internal/mail" "codex-pool/internal/mail"
utls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
) )
func init() { func init() {
@@ -36,7 +30,7 @@ var UseFixedAuthURL = false
// CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现 // CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现
type CodexAPIAuth struct { type CodexAPIAuth struct {
client *http.Client tlsClient *client.TLSClient
email string email string
password string password string
workspaceID string workspaceID string
@@ -47,130 +41,29 @@ type CodexAPIAuth struct {
sentinelToken string sentinelToken string
solvedPow string solvedPow string
userAgent string userAgent string
secChUa string
secChPlatform string
proxyURL string proxyURL string
logger *AuthLogger logger *AuthLogger
} }
// utlsRoundTripper - 模拟 Chrome TLS 指纹 // NewCodexAPIAuth 创建 CodexAuth 实例(使用随机 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 实例
// authURL 和 sessionID 由 S2A 生成 // authURL 和 sessionID 由 S2A 生成
func NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) { func NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) {
var proxyURL *url.URL tlsClient, err := client.New(proxy)
if proxy != "" {
var err error
proxyURL, err = url.Parse(proxy)
if err != nil { if err != nil {
return nil, fmt.Errorf("解析代理地址失败: %v", err) return nil, fmt.Errorf("创建 TLS 客户端失败: %v", err)
}
} }
jar, _ := cookiejar.New(nil) fpInfo := tlsClient.GetFingerprintInfo()
client := &http.Client{ ua, secChUa, secChPlatform := tlsClient.GetHeadersInfo()
Transport: &utlsRoundTripper{proxyURL: proxyURL},
Jar: jar, if logger != nil {
CheckRedirect: func(req *http.Request, via []*http.Request) error { logger.LogStep(StepBrowserStart, "指纹: %s", fpInfo)
return http.ErrUseLastResponse // 禁用自动重定向,手动跟随
},
Timeout: 30 * time.Second,
} }
return &CodexAPIAuth{ return &CodexAPIAuth{
client: client, tlsClient: tlsClient,
email: email, email: email,
password: password, password: password,
workspaceID: workspaceID, workspaceID: workspaceID,
@@ -178,7 +71,9 @@ func NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy str
sessionID: sessionID, sessionID: sessionID,
deviceID: generateUUID(), deviceID: generateUUID(),
sid: 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", userAgent: ua,
secChUa: secChUa,
secChPlatform: secChPlatform,
proxyURL: proxy, proxyURL: proxy,
logger: logger, logger: logger,
}, nil }, nil
@@ -278,7 +173,7 @@ func (c *CodexAPIAuth) getRequirementsToken() string {
return "gAAAAAC" + encoded + "~S" 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) { func (c *CodexAPIAuth) doRequest(method, urlStr string, body interface{}, headers map[string]string) (*http.Response, []byte, error) {
var bodyReader io.Reader var bodyReader io.Reader
if body != nil { if body != nil {
@@ -291,13 +186,17 @@ func (c *CodexAPIAuth) doRequest(method, urlStr string, body interface{}, header
return nil, nil, err return nil, nil, err
} }
// 设置默认头 // 设置默认头(使用指纹动态生成的值)
req.Header.Set("User-Agent", c.userAgent) req.Header.Set("User-Agent", c.userAgent)
req.Header.Set("Accept", "*/*") req.Header.Set("Accept", "*/*")
req.Header.Set("Accept-Language", "en-US,en;q=0.9") 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-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-dest", "empty")
req.Header.Set("sec-fetch-mode", "cors") req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-site", "same-origin") 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) req.Header.Set(k, v)
} }
resp, err := c.client.Do(req) resp, err := c.tlsClient.Do(req)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
@@ -744,7 +643,7 @@ func min(a, b int) int {
return b return b
} }
// CompleteWithCodexAPI 使用纯 API 方式完成授权 // CompleteWithCodexAPI 使用纯 API 方式完成授权(带 403 重试换指纹机制)
// authURL 和 sessionID 由 S2A 生成 // authURL 和 sessionID 由 S2A 生成
func CompleteWithCodexAPI(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (string, error) { func CompleteWithCodexAPI(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (string, error) {
if logger != nil { if logger != nil {
@@ -755,18 +654,46 @@ func CompleteWithCodexAPI(email, password, workspaceID, authURL, sessionID, prox
} }
} }
// 403 重试机制 - 最多重试 3 次,每次换新指纹
var lastErr error
for retry := 0; retry < 3; retry++ {
auth, err := NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy, logger) auth, err := NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy, logger)
if err != nil { if err != nil {
lastErr = err
if logger != nil { if logger != nil {
logger.LogError(StepBrowserStart, "创建 CodexAuth 失败: %v", err) logger.LogError(StepBrowserStart, "创建 CodexAuth 失败: %v", err)
} }
return "", err if retry < 2 {
if logger != nil {
logger.LogStep(StepBrowserStart, "重试 %d/3换新指纹...", retry+1)
}
continue
}
return "", fmt.Errorf("创建 CodexAuth 失败 (已重试3次): %v", err)
} }
code, err := auth.ObtainAuthorizationCode() code, err := auth.ObtainAuthorizationCode()
if err != nil { 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 return "", err
} }
auth.tlsClient.Close()
return code, nil return code, nil
}
return "", fmt.Errorf("授权失败 (已重试3次): %v", lastErr)
} }

View File

@@ -588,6 +588,15 @@ func (c *TLSClient) GetFingerprintInfo() string {
return fmt.Sprintf("%s/%s %s (%s)", typeName, fp.Browser, fp.Version, fp.Platform) 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 关闭客户端 // Close 关闭客户端
func (c *TLSClient) Close() { func (c *TLSClient) Close() {
if c.azureSession != nil { if c.azureSession != nil {