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 (
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user