feat: Add a new Go backend module for API-based authentication, featuring TLS fingerprinting and Proof-of-Work solving.

This commit is contained in:
2026-02-02 09:58:29 +08:00
parent 8fbafee79a
commit 54ccd4a100
3 changed files with 293 additions and 187 deletions

1
.gitignore vendored
View File

@@ -102,3 +102,4 @@ backend/codex-pool.exe
backend/codex-pool.exe backend/codex-pool.exe
.claude/settings.local.json .claude/settings.local.json
CodexAuth CodexAuth
get_code.go

View File

@@ -13,6 +13,7 @@ require (
github.com/go-rod/rod v0.116.2 github.com/go-rod/rod v0.116.2
github.com/go-rod/stealth v0.4.9 github.com/go-rod/stealth v0.4.9
github.com/mattn/go-sqlite3 v1.14.33 github.com/mattn/go-sqlite3 v1.14.33
github.com/refraction-networking/utls v1.6.7
) )
require ( require (

View File

@@ -2,24 +2,30 @@ package auth
import ( import (
"bytes" "bytes"
"context"
"crypto/sha256" "crypto/sha256"
"crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
"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" utls "github.com/refraction-networking/utls"
"golang.org/x/net/http2"
) )
// 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义 // 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义
// CodexAPIAuth 纯 API 授权 (无浏览器) // CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现
type CodexAPIAuth struct { type CodexAPIAuth struct {
client *client.TLSClient client *http.Client
email string email string
password string password string
workspaceID string workspaceID string
@@ -28,24 +34,126 @@ type CodexAPIAuth struct {
sentinelToken string sentinelToken string
solvedPow string solvedPow string
userAgent string userAgent string
proxyURL string
logger *AuthLogger 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\r\n", targetHost, targetHost)
_, 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 实例
func NewCodexAPIAuth(email, password, workspaceID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) { func NewCodexAPIAuth(email, password, workspaceID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) {
tlsClient, err := client.New(proxy) var proxyURL *url.URL
if proxy != "" {
var err error
proxyURL, err = url.Parse(proxy)
if err != nil { if err != nil {
return nil, fmt.Errorf("创建 TLS 客户端失败: %v", err) return nil, fmt.Errorf("解析代理地址失败: %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,
} }
return &CodexAPIAuth{ return &CodexAPIAuth{
client: tlsClient, client: client,
email: email, email: email,
password: password, password: password,
workspaceID: workspaceID, workspaceID: workspaceID,
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/131.0.0.0 Safari/537.36", 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, logger: logger,
}, nil }, nil
} }
@@ -54,9 +162,8 @@ func NewCodexAPIAuth(email, password, workspaceID, proxy string, logger *AuthLog
func generateUUID() string { func generateUUID() string {
b := make([]byte, 16) b := make([]byte, 16)
rand.Read(b) rand.Read(b)
b[6] = (b[6] & 0x0f) | 0x40 return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
b[8] = (b[8] & 0x3f) | 0x80 b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
} }
// generateCodeVerifier 生成 PKCE code_verifier // generateCodeVerifier 生成 PKCE code_verifier
@@ -96,14 +203,19 @@ func fnv1a32(data []byte) uint32 {
// getParseTime 生成 JS Date().toString() 格式的时间字符串 // getParseTime 生成 JS Date().toString() 格式的时间字符串
func getParseTime() string { func getParseTime() string {
now := time.Now() loc, _ := time.LoadLocation("Asia/Shanghai")
return now.Format("Mon Jan 02 2006 15:04:05") + " GMT+0800 (中国标准时间)" now := time.Now().In(loc)
days := []string{"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}
months := []string{"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}
return fmt.Sprintf("%s %s %02d %d %02d:%02d:%02d GMT+0800 (中国标准时间)",
days[now.Weekday()], months[now.Month()-1], now.Day(), now.Year(),
now.Hour(), now.Minute(), now.Second())
} }
// getConfig 生成 PoW 配置数组 // getConfig 生成 PoW 配置数组
func (c *CodexAPIAuth) getConfig() []interface{} { func (c *CodexAPIAuth) getConfig() []interface{} {
return []interface{}{ return []interface{}{
2500 + rand.Intn(1000), rand.Intn(1000) + 2500,
getParseTime(), getParseTime(),
4294967296, 4294967296,
0, 0,
@@ -114,13 +226,13 @@ func (c *CodexAPIAuth) getConfig() []interface{} {
"zh-CN", "zh-CN",
0, 0,
"canSharefunction canShare() { [native code] }", "canSharefunction canShare() { [native code] }",
fmt.Sprintf("_reactListening%d", 1000000+rand.Intn(9000000)), fmt.Sprintf("_reactListening%d", rand.Intn(9000000)+1000000),
"onhashchange", "onhashchange",
float64(time.Now().UnixNano()/1e6) / 1000.0, float64(time.Now().UnixNano()) / 1e6,
c.sid, c.sid,
"", "",
24, 24,
time.Now().UnixMilli() - int64(10000+rand.Intn(40000)), time.Now().UnixMilli() - int64(rand.Intn(40000)+10000),
} }
} }
@@ -135,7 +247,8 @@ func (c *CodexAPIAuth) solvePow(seed, difficulty string, cfg []interface{}, maxI
jsonStr, _ := json.Marshal(cfg) jsonStr, _ := json.Marshal(cfg)
encoded := base64.StdEncoding.EncodeToString(jsonStr) encoded := base64.StdEncoding.EncodeToString(jsonStr)
h := fnv1a32(append(seedBytes, []byte(encoded)...)) combined := append(seedBytes, []byte(encoded)...)
h := fnv1a32(combined)
hexHash := fmt.Sprintf("%08x", h) hexHash := fmt.Sprintf("%08x", h)
if hexHash[:len(difficulty)] <= difficulty { if hexHash[:len(difficulty)] <= difficulty {
@@ -156,75 +269,100 @@ func (c *CodexAPIAuth) getRequirementsToken() string {
return "gAAAAAC" + encoded + "~S" return "gAAAAAC" + encoded + "~S"
} }
// doRequest 执行 HTTP 请求
func (c *CodexAPIAuth) doRequest(method, urlStr string, body interface{}, headers map[string]string) (*http.Response, []byte, error) {
var bodyReader io.Reader
if body != nil {
jsonBody, _ := json.Marshal(body)
bodyReader = bytes.NewReader(jsonBody)
}
req, err := http.NewRequest(method, urlStr, bodyReader)
if err != nil {
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"`)
req.Header.Set("sec-ch-ua-mobile", "?0")
req.Header.Set("sec-ch-ua-platform", `"Windows"`)
req.Header.Set("sec-fetch-dest", "empty")
req.Header.Set("sec-fetch-mode", "cors")
req.Header.Set("sec-fetch-site", "same-origin")
for k, v := range headers {
req.Header.Set(k, v)
}
resp, err := c.client.Do(req)
if err != nil {
return nil, nil, err
}
defer resp.Body.Close()
respBody, _ := io.ReadAll(resp.Body)
return resp, respBody, nil
}
// callSentinelReq 调用 Sentinel 获取 token // callSentinelReq 调用 Sentinel 获取 token
func (c *CodexAPIAuth) callSentinelReq(flow string) error { func (c *CodexAPIAuth) callSentinelReq(flow string) bool {
initToken := c.getRequirementsToken() initToken := c.getRequirementsToken()
payload := map[string]interface{}{ payload := map[string]string{
"p": initToken, "p": initToken,
"id": c.deviceID, "id": c.deviceID,
"flow": flow, "flow": flow,
} }
body, _ := json.Marshal(payload) headers := map[string]string{"Content-Type": "application/json"}
req, _ := http.NewRequest("POST", "https://sentinel.openai.com/backend-api/sentinel/req", bytes.NewReader(body)) resp, body, err := c.doRequest("POST", "https://sentinel.openai.com/backend-api/sentinel/req", payload, headers)
req.Header.Set("Content-Type", "application/json") if err != nil || resp.StatusCode != 200 {
req.Header.Set("Accept", "*/*") c.logError(StepNavigate, "Sentinel 请求失败: %v (status: %d)", err, resp.StatusCode)
req.Header.Set("Origin", "https://auth.openai.com") return false
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Site", "cross-site")
resp, err := c.client.Do(req)
if err != nil {
return fmt.Errorf("sentinel 请求失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("sentinel 状态码: %d", resp.StatusCode)
} }
respBody, _ := client.ReadBody(resp) var data map[string]interface{}
var result struct { json.Unmarshal(body, &data)
Token string `json:"token"`
ProofOfWork struct { if token, ok := data["token"].(string); ok {
Required bool `json:"required"` c.sentinelToken = token
Seed string `json:"seed"`
Difficulty string `json:"difficulty"`
} `json:"proofofwork"`
} }
if err := json.Unmarshal(respBody, &result); err != nil { if powReq, ok := data["proofofwork"].(map[string]interface{}); ok {
return fmt.Errorf("解析 sentinel 响应失败: %v", err) if required, ok := powReq["required"].(bool); ok && required {
} seed, _ := powReq["seed"].(string)
difficulty, _ := powReq["difficulty"].(string)
c.sentinelToken = result.Token config := c.getConfig()
solved := c.solvePow(seed, difficulty, config, 5000000)
if result.ProofOfWork.Required { if solved != "" {
cfg := c.getConfig()
solved := c.solvePow(result.ProofOfWork.Seed, result.ProofOfWork.Difficulty, cfg, 5000000)
if solved == "" {
return fmt.Errorf("PoW 解决失败")
}
c.solvedPow = "gAAAAAB" + solved c.solvedPow = "gAAAAAB" + solved
} else {
c.logError(StepNavigate, "PoW 求解失败")
return false
}
} else {
c.solvedPow = initToken
}
} else { } else {
c.solvedPow = initToken c.solvedPow = initToken
} }
return nil return true
} }
// getSentinelHeader 构建 sentinel header // getSentinelHeader 构建 sentinel header
func (c *CodexAPIAuth) getSentinelHeader(flow string) string { func (c *CodexAPIAuth) getSentinelHeader(flow string) string {
obj := map[string]interface{}{ sentinelObj := map[string]string{
"p": c.solvedPow, "p": c.solvedPow,
"id": c.deviceID, "id": c.deviceID,
"flow": flow, "flow": flow,
} }
if c.sentinelToken != "" { if c.sentinelToken != "" {
obj["c"] = c.sentinelToken sentinelObj["c"] = c.sentinelToken
} }
header, _ := json.Marshal(obj) header, _ := json.Marshal(sentinelObj)
return string(header) return string(header)
} }
@@ -242,17 +380,6 @@ func (c *CodexAPIAuth) logError(step AuthStep, format string, args ...interface{
} }
} }
// setAPIHeaders 设置 API 请求的通用头 (模拟 XHR 请求而非页面导航)
func (c *CodexAPIAuth) setAPIHeaders(req *http.Request, referer string) {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "*/*")
req.Header.Set("Origin", "https://auth.openai.com")
req.Header.Set("Referer", referer)
req.Header.Set("Sec-Fetch-Dest", "empty")
req.Header.Set("Sec-Fetch-Mode", "cors")
req.Header.Set("Sec-Fetch-Site", "same-origin")
}
// ObtainAuthorizationCode 获取授权码 // ObtainAuthorizationCode 获取授权码
func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
c.logStep(StepNavigate, "开始 Codex API 授权流程...") c.logStep(StepNavigate, "开始 Codex API 授权流程...")
@@ -278,158 +405,134 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
authURL := "https://auth.openai.com/oauth/authorize?" + params.Encode() authURL := "https://auth.openai.com/oauth/authorize?" + params.Encode()
// 3. 访问授权页面 headers := map[string]string{
"Origin": "https://auth.openai.com",
"Referer": "https://auth.openai.com/log-in",
"Content-Type": "application/json",
}
// 3. 访问授权页面并手动跟随重定向
c.logStep(StepNavigate, "访问授权页面...") c.logStep(StepNavigate, "访问授权页面...")
req, _ := http.NewRequest("GET", authURL, nil) resp, _, err := c.doRequest("GET", authURL, nil, headers)
req.Header.Set("Referer", "https://auth.openai.com/log-in")
resp, err := c.client.Do(req)
if err != nil { if err != nil {
c.logError(StepNavigate, "访问授权页失败: %v", err) c.logError(StepNavigate, "访问授权页失败: %v", err)
return "", fmt.Errorf("访问授权页失败: %v", err) return "", fmt.Errorf("访问授权页失败: %v", err)
} }
defer resp.Body.Close()
client.ReadBody(resp) // 消耗响应体 // 手动跟随重定向
referer := resp.Request.URL.String() currentURL := authURL
for resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location")
if location == "" {
break
}
if !strings.HasPrefix(location, "http") {
parsedURL, _ := url.Parse(currentURL)
location = parsedURL.Scheme + "://" + parsedURL.Host + location
}
currentURL = location
resp, _, err = c.doRequest("GET", currentURL, nil, headers)
if err != nil {
break
}
}
headers["Referer"] = currentURL
// 4. 提交邮箱 // 4. 提交邮箱
c.logStep(StepInputEmail, "提交邮箱: %s", c.email) c.logStep(StepInputEmail, "提交邮箱: %s", c.email)
if err := c.callSentinelReq("login_web_init"); err != nil { if !c.callSentinelReq("login_web_init") {
c.logError(StepInputEmail, "Sentinel 请求失败: %v", err) return "", fmt.Errorf("Sentinel 请求失败")
return "", err
} }
authHeaders := make(map[string]string)
for k, v := range headers {
authHeaders[k] = v
}
authHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("authorize_continue")
emailPayload := map[string]interface{}{ emailPayload := map[string]interface{}{
"username": map[string]string{ "username": map[string]string{
"kind": "email", "kind": "email",
"value": c.email, "value": c.email,
}, },
} }
emailBody, _ := json.Marshal(emailPayload)
req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/authorize/continue", bytes.NewReader(emailBody)) resp, body, err := c.doRequest("POST", "https://auth.openai.com/api/accounts/authorize/continue", emailPayload, authHeaders)
c.setAPIHeaders(req, referer) if err != nil || resp.StatusCode != 200 {
req.Header.Set("OpenAI-Sentinel-Token", c.getSentinelHeader("authorize_continue")) c.logError(StepInputEmail, "提交邮箱失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
resp, err = c.client.Do(req)
if err != nil {
c.logError(StepInputEmail, "提交邮箱失败: %v", err)
return "", fmt.Errorf("提交邮箱失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := client.ReadBody(resp)
c.logError(StepInputEmail, "提交邮箱失败: %d - %s", resp.StatusCode, string(body))
return "", fmt.Errorf("提交邮箱失败: %d", resp.StatusCode) return "", fmt.Errorf("提交邮箱失败: %d", resp.StatusCode)
} }
// 解析响应,检查是否需要密码 var data map[string]interface{}
emailResp, _ := client.ReadBody(resp) json.Unmarshal(body, &data)
var emailResult map[string]interface{}
json.Unmarshal(emailResp, &emailResult)
// 5. 验证密码 // 检查是否需要密码验证
c.logStep(StepInputPassword, "验证密码...") pageType := ""
if err := c.callSentinelReq("authorize_continue__auto"); err != nil { if page, ok := data["page"].(map[string]interface{}); ok {
c.logError(StepInputPassword, "Sentinel 请求失败: %v", err) if pt, ok := page["type"].(string); ok {
return "", err pageType = pt
}
} }
pwdPayload := map[string]string{ if pageType == "password" || strings.Contains(string(body), "password") {
// 5. 验证密码
c.logStep(StepInputPassword, "验证密码...")
if !c.callSentinelReq("authorize_continue__auto") {
return "", fmt.Errorf("Sentinel 请求失败")
}
verifyHeaders := make(map[string]string)
for k, v := range headers {
verifyHeaders[k] = v
}
verifyHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("password_verify")
passwordPayload := map[string]string{
"username": c.email, "username": c.email,
"password": c.password, "password": c.password,
} }
pwdBody, _ := json.Marshal(pwdPayload)
req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/password/verify", bytes.NewReader(pwdBody)) resp, body, err = c.doRequest("POST", "https://auth.openai.com/api/accounts/password/verify", passwordPayload, verifyHeaders)
c.setAPIHeaders(req, referer) if err != nil || resp.StatusCode != 200 {
req.Header.Set("OpenAI-Sentinel-Token", c.getSentinelHeader("password_verify")) c.logError(StepInputPassword, "密码验证失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
resp, err = c.client.Do(req)
if err != nil {
c.logError(StepInputPassword, "验证密码失败: %v", err)
return "", fmt.Errorf("验证密码失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
body, _ := client.ReadBody(resp)
c.logError(StepInputPassword, "密码验证失败: %d - %s", resp.StatusCode, string(body))
return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode) return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode)
} }
}
// 6. 选择工作区 // 6. 选择工作区
c.logStep(StepSelectWorkspace, "选择工作区: %s", c.workspaceID) c.logStep(StepSelectWorkspace, "选择工作区: %s", c.workspaceID)
if err := c.callSentinelReq("password_verify__auto"); err != nil { if !c.callSentinelReq("password_verify__auto") {
c.logError(StepSelectWorkspace, "Sentinel 请求失败: %v", err) return "", fmt.Errorf("Sentinel 请求失败")
return "", err
} }
wsPayload := map[string]string{ workspacePayload := map[string]string{
"workspace_id": c.workspaceID, "workspace_id": c.workspaceID,
} }
wsBody, _ := json.Marshal(wsPayload)
req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/workspace/select", bytes.NewReader(wsBody)) resp, body, err = c.doRequest("POST", "https://auth.openai.com/api/accounts/workspace/select", workspacePayload, headers)
c.setAPIHeaders(req, referer) if err != nil || resp.StatusCode != 200 {
c.logError(StepSelectWorkspace, "选择工作区失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
resp, err = c.client.Do(req)
if err != nil {
c.logError(StepSelectWorkspace, "选择工作区失败: %v", err)
return "", fmt.Errorf("选择工作区失败: %v", err)
}
defer resp.Body.Close()
// 调试: 打印响应头信息
c.logStep(StepSelectWorkspace, "响应状态: %d, Content-Length: %s, Content-Encoding: %s",
resp.StatusCode, resp.Header.Get("Content-Length"), resp.Header.Get("Content-Encoding"))
if resp.StatusCode != 200 {
body, _ := client.ReadBody(resp)
c.logError(StepSelectWorkspace, "选择工作区失败: %d - %s", resp.StatusCode, string(body))
return "", fmt.Errorf("选择工作区失败: %d", resp.StatusCode) return "", fmt.Errorf("选择工作区失败: %d", resp.StatusCode)
} }
// 解析 continue_url json.Unmarshal(body, &data)
wsResp, _ := client.ReadBody(resp) continueURL, ok := data["continue_url"].(string)
c.logStep(StepSelectWorkspace, "工作区响应: %s", string(wsResp)) if !ok || continueURL == "" {
c.logError(StepSelectWorkspace, "未获取到 continue_url, 响应: %s", string(body[:min(500, len(body))]))
var wsResult struct {
ContinueURL string `json:"continue_url"`
Page struct {
Type string `json:"type"`
} `json:"page"`
Error string `json:"error"`
Message string `json:"message"`
}
if err := json.Unmarshal(wsResp, &wsResult); err != nil {
c.logError(StepSelectWorkspace, "解析响应失败: %v, 原始: %s", err, string(wsResp))
return "", fmt.Errorf("解析响应失败: %v", err)
}
if wsResult.ContinueURL == "" {
c.logError(StepSelectWorkspace, "未获取到 continue_url, page=%s, error=%s, msg=%s",
wsResult.Page.Type, wsResult.Error, wsResult.Message)
return "", fmt.Errorf("未获取到 continue_url") return "", fmt.Errorf("未获取到 continue_url")
} }
// 7. 跟随重定向获取授权码 // 7. 跟随重定向获取授权码
c.logStep(StepWaitCallback, "跟随重定向...") c.logStep(StepWaitCallback, "跟随重定向...")
continueURL := wsResult.ContinueURL
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
req, _ = http.NewRequest("GET", continueURL, nil) resp, _, err = c.doRequest("GET", continueURL, nil, headers)
resp, err = c.client.Do(req)
if err != nil { if err != nil {
break break
} }
if resp.StatusCode >= 300 && resp.StatusCode < 400 {
location := resp.Header.Get("Location") location := resp.Header.Get("Location")
resp.Body.Close()
if resp.StatusCode >= 301 && resp.StatusCode <= 308 {
if strings.Contains(location, "localhost:1455") { if strings.Contains(location, "localhost:1455") {
// 提取授权码
code := ExtractCodeFromCallbackURL(location) code := ExtractCodeFromCallbackURL(location)
if code != "" { if code != "" {
c.logStep(StepComplete, "授权成功,获取到授权码") c.logStep(StepComplete, "授权成功,获取到授权码")
@@ -456,24 +559,18 @@ func (c *CodexAPIAuth) ExchangeCodeForTokens(code, codeVerifier string) (*CodexT
"redirect_uri": CodexRedirectURI, "redirect_uri": CodexRedirectURI,
} }
body, _ := json.Marshal(payload) headers := map[string]string{"Content-Type": "application/json"}
req, _ := http.NewRequest("POST", "https://auth.openai.com/oauth/token", bytes.NewReader(body)) resp, body, err := c.doRequest("POST", "https://auth.openai.com/oauth/token", payload, headers)
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil { if err != nil {
return nil, fmt.Errorf("token 交换失败: %v", err) return nil, fmt.Errorf("token 交换失败: %v", err)
} }
defer resp.Body.Close()
if resp.StatusCode != 200 { if resp.StatusCode != 200 {
respBody, _ := client.ReadBody(resp) return nil, fmt.Errorf("token 交换失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
return nil, fmt.Errorf("token 交换失败: %d - %s", resp.StatusCode, string(respBody))
} }
respBody, _ := client.ReadBody(resp)
var tokens CodexTokens var tokens CodexTokens
if err := json.Unmarshal(respBody, &tokens); err != nil { if err := json.Unmarshal(body, &tokens); err != nil {
return nil, fmt.Errorf("解析 token 失败: %v", err) return nil, fmt.Errorf("解析 token 失败: %v", err)
} }
@@ -485,7 +582,14 @@ func (c *CodexAPIAuth) ExchangeCodeForTokens(code, codeVerifier string) (*CodexT
return &tokens, nil return &tokens, nil
} }
// CodexTokens 结构体已在 s2a.go 中定义 // min 返回较小值
func min(a, b int) int {
if a < b {
return a
}
return b
}
// CompleteWithCodexAPI 使用纯 API 方式完成授权 // CompleteWithCodexAPI 使用纯 API 方式完成授权
func CompleteWithCodexAPI(email, password, workspaceID, proxy string, logger *AuthLogger) (string, error) { func CompleteWithCodexAPI(email, password, workspaceID, proxy string, logger *AuthLogger) (string, error) {
if logger != nil { if logger != nil {