Files
codexautopool/backend/internal/auth/codex_api.go

510 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package auth
import (
"bytes"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"fmt"
"math/rand"
"net/http"
"net/url"
"strings"
"time"
"codex-pool/internal/client"
)
// 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义
// CodexAPIAuth 纯 API 授权 (无浏览器)
type CodexAPIAuth struct {
client *client.TLSClient
email string
password string
workspaceID string
deviceID string
sid string
sentinelToken string
solvedPow string
userAgent string
logger *AuthLogger
}
// NewCodexAPIAuth 创建 CodexAuth 实例
func NewCodexAPIAuth(email, password, workspaceID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) {
tlsClient, err := client.New(proxy)
if err != nil {
return nil, fmt.Errorf("创建 TLS 客户端失败: %v", err)
}
return &CodexAPIAuth{
client: tlsClient,
email: email,
password: password,
workspaceID: workspaceID,
deviceID: 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",
logger: logger,
}, nil
}
// generateUUID 生成 UUID v4
func generateUUID() string {
b := make([]byte, 16)
rand.Read(b)
b[6] = (b[6] & 0x0f) | 0x40
b[8] = (b[8] & 0x3f) | 0x80
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
func generateCodeVerifier() string {
b := make([]byte, 64)
rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}
// generateCodeChallenge 生成 PKCE code_challenge (S256)
func generateCodeChallenge(verifier string) string {
hash := sha256.Sum256([]byte(verifier))
return base64.RawURLEncoding.EncodeToString(hash[:])
}
// generateState 生成 state
func generateState() string {
b := make([]byte, 16)
rand.Read(b)
return base64.RawURLEncoding.EncodeToString(b)
}
// fnv1a32 FNV-1a 32-bit hash
func fnv1a32(data []byte) uint32 {
h := uint32(2166136261)
for _, b := range data {
h ^= uint32(b)
h *= 16777619
}
h ^= (h >> 16)
h *= 2246822507
h ^= (h >> 13)
h *= 3266489909
h ^= (h >> 16)
return h
}
// getParseTime 生成 JS Date().toString() 格式的时间字符串
func getParseTime() string {
now := time.Now()
return now.Format("Mon Jan 02 2006 15:04:05") + " GMT+0800 (中国标准时间)"
}
// getConfig 生成 PoW 配置数组
func (c *CodexAPIAuth) getConfig() []interface{} {
return []interface{}{
2500 + rand.Intn(1000),
getParseTime(),
4294967296,
0,
c.userAgent,
"chrome-extension://pgojnojmmhpofjgdmaebadhbocahppod/assets/aW5qZWN0X2hhc2g/aW5qZ",
nil,
"zh-CN",
"zh-CN",
0,
"canSharefunction canShare() { [native code] }",
fmt.Sprintf("_reactListening%d", 1000000+rand.Intn(9000000)),
"onhashchange",
float64(time.Now().UnixNano()/1e6) / 1000.0,
c.sid,
"",
24,
time.Now().UnixMilli() - int64(10000+rand.Intn(40000)),
}
}
// solvePow 解决 PoW 挑战
func (c *CodexAPIAuth) solvePow(seed, difficulty string, cfg []interface{}, maxIterations int) string {
seedBytes := []byte(seed)
for i := 0; i < maxIterations; i++ {
cfg[3] = i
cfg[9] = 0
jsonStr, _ := json.Marshal(cfg)
encoded := base64.StdEncoding.EncodeToString(jsonStr)
h := fnv1a32(append(seedBytes, []byte(encoded)...))
hexHash := fmt.Sprintf("%08x", h)
if hexHash[:len(difficulty)] <= difficulty {
return encoded + "~S"
}
}
return ""
}
// getRequirementsToken 生成初始 token
func (c *CodexAPIAuth) getRequirementsToken() string {
cfg := c.getConfig()
cfg[3] = 0
cfg[9] = 0
jsonStr, _ := json.Marshal(cfg)
encoded := base64.StdEncoding.EncodeToString(jsonStr)
return "gAAAAAC" + encoded + "~S"
}
// callSentinelReq 调用 Sentinel 获取 token
func (c *CodexAPIAuth) callSentinelReq(flow string) error {
initToken := c.getRequirementsToken()
payload := map[string]interface{}{
"p": initToken,
"id": c.deviceID,
"flow": flow,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://sentinel.openai.com/backend-api/sentinel/req", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "*/*")
req.Header.Set("Origin", "https://auth.openai.com")
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 result struct {
Token string `json:"token"`
ProofOfWork struct {
Required bool `json:"required"`
Seed string `json:"seed"`
Difficulty string `json:"difficulty"`
} `json:"proofofwork"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return fmt.Errorf("解析 sentinel 响应失败: %v", err)
}
c.sentinelToken = result.Token
if result.ProofOfWork.Required {
cfg := c.getConfig()
solved := c.solvePow(result.ProofOfWork.Seed, result.ProofOfWork.Difficulty, cfg, 5000000)
if solved == "" {
return fmt.Errorf("PoW 解决失败")
}
c.solvedPow = "gAAAAAB" + solved
} else {
c.solvedPow = initToken
}
return nil
}
// getSentinelHeader 构建 sentinel header
func (c *CodexAPIAuth) getSentinelHeader(flow string) string {
obj := map[string]interface{}{
"p": c.solvedPow,
"id": c.deviceID,
"flow": flow,
}
if c.sentinelToken != "" {
obj["c"] = c.sentinelToken
}
header, _ := json.Marshal(obj)
return string(header)
}
// logStep 记录日志
func (c *CodexAPIAuth) logStep(step AuthStep, format string, args ...interface{}) {
if c.logger != nil {
c.logger.LogStep(step, format, args...)
}
}
// logError 记录错误
func (c *CodexAPIAuth) logError(step AuthStep, format string, args ...interface{}) {
if c.logger != nil {
c.logger.LogError(step, format, args...)
}
}
// 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 获取授权码
func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
c.logStep(StepNavigate, "开始 Codex API 授权流程...")
// 1. 生成 PKCE 参数
codeVerifier := generateCodeVerifier()
codeChallenge := generateCodeChallenge(codeVerifier)
state := generateState()
// 2. 构建授权 URL
params := url.Values{
"client_id": {CodexClientID},
"scope": {CodexScope},
"response_type": {"code"},
"redirect_uri": {CodexRedirectURI},
"code_challenge": {codeChallenge},
"code_challenge_method": {"S256"},
"state": {state},
"id_token_add_organizations": {"true"},
"codex_cli_simplified_flow": {"true"},
"originator": {"codex_cli_rs"},
}
authURL := "https://auth.openai.com/oauth/authorize?" + params.Encode()
// 3. 访问授权页面
c.logStep(StepNavigate, "访问授权页面...")
req, _ := http.NewRequest("GET", authURL, nil)
req.Header.Set("Referer", "https://auth.openai.com/log-in")
resp, err := c.client.Do(req)
if err != nil {
c.logError(StepNavigate, "访问授权页失败: %v", err)
return "", fmt.Errorf("访问授权页失败: %v", err)
}
defer resp.Body.Close()
client.ReadBody(resp) // 消耗响应体
referer := resp.Request.URL.String()
// 4. 提交邮箱
c.logStep(StepInputEmail, "提交邮箱: %s", c.email)
if err := c.callSentinelReq("login_web_init"); err != nil {
c.logError(StepInputEmail, "Sentinel 请求失败: %v", err)
return "", err
}
emailPayload := map[string]interface{}{
"username": map[string]string{
"kind": "email",
"value": c.email,
},
}
emailBody, _ := json.Marshal(emailPayload)
req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/authorize/continue", bytes.NewReader(emailBody))
c.setAPIHeaders(req, referer)
req.Header.Set("OpenAI-Sentinel-Token", c.getSentinelHeader("authorize_continue"))
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)
}
// 解析响应,检查是否需要密码
emailResp, _ := client.ReadBody(resp)
var emailResult map[string]interface{}
json.Unmarshal(emailResp, &emailResult)
// 5. 验证密码
c.logStep(StepInputPassword, "验证密码...")
if err := c.callSentinelReq("authorize_continue__auto"); err != nil {
c.logError(StepInputPassword, "Sentinel 请求失败: %v", err)
return "", err
}
pwdPayload := map[string]string{
"username": c.email,
"password": c.password,
}
pwdBody, _ := json.Marshal(pwdPayload)
req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/password/verify", bytes.NewReader(pwdBody))
c.setAPIHeaders(req, referer)
req.Header.Set("OpenAI-Sentinel-Token", c.getSentinelHeader("password_verify"))
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)
}
// 6. 选择工作区
c.logStep(StepSelectWorkspace, "选择工作区: %s", c.workspaceID)
if err := c.callSentinelReq("password_verify__auto"); err != nil {
c.logError(StepSelectWorkspace, "Sentinel 请求失败: %v", err)
return "", err
}
wsPayload := map[string]string{
"workspace_id": c.workspaceID,
}
wsBody, _ := json.Marshal(wsPayload)
req, _ = http.NewRequest("POST", "https://auth.openai.com/api/accounts/workspace/select", bytes.NewReader(wsBody))
c.setAPIHeaders(req, referer)
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)
}
// 解析 continue_url
wsResp, _ := client.ReadBody(resp)
c.logStep(StepSelectWorkspace, "工作区响应: %s", string(wsResp))
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")
}
// 7. 跟随重定向获取授权码
c.logStep(StepWaitCallback, "跟随重定向...")
continueURL := wsResult.ContinueURL
for i := 0; i < 10; i++ {
req, _ = http.NewRequest("GET", continueURL, nil)
resp, err = c.client.Do(req)
if err != nil {
break
}
location := resp.Header.Get("Location")
resp.Body.Close()
if resp.StatusCode >= 301 && resp.StatusCode <= 308 {
if strings.Contains(location, "localhost:1455") {
// 提取授权码
code := ExtractCodeFromCallbackURL(location)
if code != "" {
c.logStep(StepComplete, "授权成功,获取到授权码")
return code, nil
}
}
continueURL = location
} else {
break
}
}
c.logError(StepWaitCallback, "未能获取授权码")
return "", fmt.Errorf("未能获取授权码")
}
// ExchangeCodeForTokens 用授权码换取 tokens
func (c *CodexAPIAuth) ExchangeCodeForTokens(code, codeVerifier string) (*CodexTokens, error) {
payload := map[string]string{
"grant_type": "authorization_code",
"client_id": CodexClientID,
"code_verifier": codeVerifier,
"code": code,
"redirect_uri": CodexRedirectURI,
}
body, _ := json.Marshal(payload)
req, _ := http.NewRequest("POST", "https://auth.openai.com/oauth/token", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("token 交换失败: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
respBody, _ := client.ReadBody(resp)
return nil, fmt.Errorf("token 交换失败: %d - %s", resp.StatusCode, string(respBody))
}
respBody, _ := client.ReadBody(resp)
var tokens CodexTokens
if err := json.Unmarshal(respBody, &tokens); err != nil {
return nil, fmt.Errorf("解析 token 失败: %v", err)
}
if tokens.ExpiresIn > 0 {
expiredAt := time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second)
tokens.ExpiredAt = expiredAt.Format(time.RFC3339)
}
return &tokens, nil
}
// CodexTokens 结构体已在 s2a.go 中定义
// CompleteWithCodexAPI 使用纯 API 方式完成授权
func CompleteWithCodexAPI(email, password, workspaceID, proxy string, logger *AuthLogger) (string, error) {
if logger != nil {
logger.LogStep(StepBrowserStart, "使用 CodexAuth API 模式...")
}
auth, err := NewCodexAPIAuth(email, password, workspaceID, proxy, logger)
if err != nil {
if logger != nil {
logger.LogError(StepBrowserStart, "创建 CodexAuth 失败: %v", err)
}
return "", err
}
code, err := auth.ObtainAuthorizationCode()
if err != nil {
return "", err
}
return code, nil
}