feat: add a new TLS client with browser fingerprinting and implement Codex API authentication.
This commit is contained in:
@@ -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,130 +41,29 @@ 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)
|
||||
tlsClient, err := client.New(proxy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析代理地址失败: %v", err)
|
||||
}
|
||||
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,
|
||||
tlsClient: tlsClient,
|
||||
email: email,
|
||||
password: password,
|
||||
workspaceID: workspaceID,
|
||||
@@ -178,7 +71,9 @@ func NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy str
|
||||
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",
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
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()
|
||||
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
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("授权失败 (已重试3次): %v", lastErr)
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user