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 (
"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)
}

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)
}
// 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 {