package auth import ( "bytes" "encoding/base64" "encoding/json" "fmt" "io" "math/rand" "net/http" "net/url" "strings" "time" "codex-pool/internal/client" "codex-pool/internal/mail" ) func init() { rand.Seed(time.Now().UnixNano()) } // FixedAuthURL 固定的授权 URL(用于测试,绕过 S2A) const FixedAuthURL = "https://auth.openai.com/oauth/authorize?client_id=app_EMoamEEZ73f0CkXaXp7hrann&code_challenge=fEepJO0_NJiqP-_FC_HLH-aqZsq68JeFtNvYY6q3qbQ&code_challenge_method=S256&codex_cli_simplified_flow=true&id_token_add_organizations=true&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&response_type=code&scope=openid+profile+email+offline_access&state=da3ec35b7368c91193d27c90e3ecd0c4fa45bebd430bcc6b5c236461d2742e93" // UseFixedAuthURL 是否使用固定的授权 URL(设为 true 可绕过 S2A 进行测试) var UseFixedAuthURL = false // 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义 // CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现 type CodexAPIAuth struct { tlsClient *client.TLSClient email string password string workspaceID string authURL string // S2A 生成的授权 URL sessionID string // S2A 会话 ID deviceID string sid string sentinelToken string solvedPow string userAgent string secChUa string secChPlatform string proxyURL string logger *AuthLogger } // NewCodexAPIAuth 创建 CodexAuth 实例(使用随机 TLS 指纹) // authURL 和 sessionID 由 S2A 生成 func NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (*CodexAPIAuth, error) { tlsClient, err := client.New(proxy) if err != nil { return nil, fmt.Errorf("创建 TLS 客户端失败: %v", err) } fpInfo := tlsClient.GetFingerprintInfo() ua, secChUa, secChPlatform := tlsClient.GetHeadersInfo() if logger != nil { logger.LogStep(StepBrowserStart, "指纹: %s", fpInfo) } return &CodexAPIAuth{ 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 } // generateUUID 生成 UUID v4 func generateUUID() string { b := make([]byte, 16) rand.Read(b) return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) } // 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 { loc, _ := time.LoadLocation("Asia/Shanghai") 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 配置数组 func (c *CodexAPIAuth) getConfig() []interface{} { return []interface{}{ rand.Intn(1000) + 2500, getParseTime(), 4294967296, 0, c.userAgent, "chrome-extension://pgojnojmmhpofjgdmaebadhbocahppod/assets/aW5qZWN0X2hhc2g/aW5qZ", nil, "zh-CN", "zh-CN", 0, "canShare−function canShare() { [native code] }", fmt.Sprintf("_reactListening%d", rand.Intn(9000000)+1000000), "onhashchange", float64(time.Now().UnixNano()) / 1e6, c.sid, "", 24, time.Now().UnixMilli() - int64(rand.Intn(40000)+10000), } } // solvePow 解决 PoW 挑战 func (c *CodexAPIAuth) solvePow(seed, difficulty string, cfg []interface{}, maxIterations int) string { startTime := time.Now() seedBytes := []byte(seed) for i := 0; i < maxIterations; i++ { cfg[3] = i cfg[9] = 0 jsonStr, _ := json.Marshal(cfg) encoded := base64.StdEncoding.EncodeToString(jsonStr) combined := append(seedBytes, []byte(encoded)...) h := fnv1a32(combined) hexHash := fmt.Sprintf("%08x", h) if hexHash[:len(difficulty)] <= difficulty { elapsed := time.Since(startTime).Seconds() c.logStep(StepNavigate, "[PoW] Solved in %.2fs (iteration %d, difficulty=%s)", elapsed, i, 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" } // 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 { 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") if c.secChUa != "" { req.Header.Set("sec-ch-ua", c.secChUa) } req.Header.Set("sec-ch-ua-mobile", "?0") 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") for k, v := range headers { req.Header.Set(k, v) } resp, err := c.tlsClient.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 func (c *CodexAPIAuth) callSentinelReq(flow string) bool { initToken := c.getRequirementsToken() payload := map[string]string{ "p": initToken, "id": c.deviceID, "flow": flow, } headers := map[string]string{"Content-Type": "application/json"} resp, body, err := c.doRequest("POST", "https://sentinel.openai.com/backend-api/sentinel/req", payload, headers) if err != nil || resp.StatusCode != 200 { c.logError(StepNavigate, "Sentinel 请求失败: %v (status: %d)", err, resp.StatusCode) return false } var data map[string]interface{} json.Unmarshal(body, &data) if token, ok := data["token"].(string); ok { c.sentinelToken = token } if powReq, ok := data["proofofwork"].(map[string]interface{}); ok { if required, ok := powReq["required"].(bool); ok && required { seed, _ := powReq["seed"].(string) difficulty, _ := powReq["difficulty"].(string) c.logStep(StepNavigate, "[Sentinel] flow=%s, PoW required, difficulty=%s", flow, difficulty) config := c.getConfig() solved := c.solvePow(seed, difficulty, config, 5000000) if solved != "" { c.solvedPow = "gAAAAAB" + solved } else { c.logError(StepNavigate, "PoW 求解失败") return false } } else { c.logStep(StepNavigate, "[Sentinel] flow=%s, PoW not required", flow) c.solvedPow = initToken } } else { c.logStep(StepNavigate, "[Sentinel] flow=%s, no PoW in response", flow) c.solvedPow = initToken } return true } // getSentinelHeader 构建 sentinel header func (c *CodexAPIAuth) getSentinelHeader(flow string) string { sentinelObj := map[string]string{ "p": c.solvedPow, "id": c.deviceID, "flow": flow, } if c.sentinelToken != "" { sentinelObj["c"] = c.sentinelToken } header, _ := json.Marshal(sentinelObj) 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...) } } // GetSessionID 获取 S2A 会话 ID(用于后续入库) func (c *CodexAPIAuth) GetSessionID() string { return c.sessionID } // ObtainAuthorizationCode 获取授权码(全局 3 分钟超时) func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { type authResult struct { code string err error } resultCh := make(chan authResult, 1) go func() { code, err := c.obtainAuthorizationCodeInternal() resultCh <- authResult{code, err} }() select { case r := <-resultCh: return r.code, r.err case <-time.After(3 * time.Minute): return "", fmt.Errorf("授权超时 (3分钟)") } } // obtainAuthorizationCodeInternal ObtainAuthorizationCode 的内部实现 func (c *CodexAPIAuth) obtainAuthorizationCodeInternal() (string, error) { c.logStep(StepNavigate, "开始 Codex API 授权流程...") // 选择使用固定 URL 还是 S2A 生成的 URL authURL := c.authURL if UseFixedAuthURL { authURL = FixedAuthURL c.logStep(StepNavigate, "使用固定授权 URL(测试模式)") } else if authURL == "" { return "", fmt.Errorf("authURL 未设置,请先通过 S2A 生成授权 URL") } headers := map[string]string{ "Origin": "https://auth.openai.com", "Referer": "https://auth.openai.com/log-in", "Content-Type": "application/json", } // 访问授权页面并手动跟随重定向 c.logStep(StepNavigate, "访问授权页面...") resp, _, err := c.doRequest("GET", authURL, nil, headers) if err != nil { c.logError(StepNavigate, "访问授权页失败: %v", err) return "", fmt.Errorf("访问授权页失败: %v", err) } // 手动跟随重定向 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 // 提交邮箱 c.logStep(StepInputEmail, "提交邮箱: %s", c.email) if !c.callSentinelReq("login_web_init") { return "", fmt.Errorf("Sentinel 请求失败") } 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{}{ "username": map[string]string{ "kind": "email", "value": c.email, }, } resp, body, err := c.doRequest("POST", "https://auth.openai.com/api/accounts/authorize/continue", emailPayload, authHeaders) if err != nil || resp.StatusCode != 200 { c.logError(StepInputEmail, "提交邮箱失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))])) return "", fmt.Errorf("提交邮箱失败: %d", resp.StatusCode) } var data map[string]interface{} json.Unmarshal(body, &data) // 检查是否需要密码验证 pageType := "" if page, ok := data["page"].(map[string]interface{}); ok { if pt, ok := page["type"].(string); ok { pageType = pt } } c.logStep(StepInputPassword, "验证密码...") if pageType == "password" || strings.Contains(string(body), "password") { // 在密码验证前记录最新邮件ID (基于 get_code.go 的增强逻辑) latestEmailID := mail.GetLatestEmailID(c.email) c.logStep(StepInputPassword, "记录当前最新邮件ID: %d", latestEmailID) // 5. 验证密码 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{ "password": c.password, } resp, body, err = c.doRequest("POST", "https://auth.openai.com/api/accounts/password/verify", passwordPayload, verifyHeaders) if err != nil || resp.StatusCode != 200 { c.logError(StepInputPassword, "密码验证失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))])) return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode) } c.logStep(StepInputPassword, "密码验证成功") // 解析密码验证响应 json.Unmarshal(body, &data) // 检查下一步是什么 nextPageType := "" if page, ok := data["page"].(map[string]interface{}); ok { if pt, ok := page["type"].(string); ok { nextPageType = pt } } // 如果需要邮箱验证,尝试获取验证码并完成验证 if nextPageType == "email_otp_verification" { c.logStep(StepInputPassword, "账号需要邮箱验证,正在获取验证码...") // 使用邮件ID过滤获取新的OTP验证码 (最多30秒) // 基于 get_code.go 的增强逻辑:使用邮件ID过滤,避免获取到旧邮件的验证码 otpCode, err := mail.GetEmailOTPAfterID(c.email, latestEmailID, 30*time.Second) if err != nil { c.logError(StepInputPassword, "获取验证码失败: %v", err) return "", fmt.Errorf("获取验证码失败: %v", err) } c.logStep(StepInputPassword, "获取到验证码: %s", otpCode) // 提交验证码到 /api/accounts/email-otp/validate // 先获取 Sentinel token (可能需要 PoW) if !c.callSentinelReq("email_otp_verification__auto") { // 如果失败,尝试 password_verify__auto if !c.callSentinelReq("password_verify__auto") { return "", fmt.Errorf("Sentinel 请求失败") } } verifyOtpHeaders := make(map[string]string) for k, v := range headers { verifyOtpHeaders[k] = v } verifyOtpHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("email_otp_validate") // 请求体格式: {"code":"016547"} otpPayload := map[string]string{ "code": otpCode, } resp, body, err = c.doRequest("POST", "https://auth.openai.com/api/accounts/email-otp/validate", otpPayload, verifyOtpHeaders) if err != nil || resp.StatusCode != 200 { c.logError(StepInputPassword, "验证码验证失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))])) return "", fmt.Errorf("验证码验证失败: %d", resp.StatusCode) } c.logStep(StepInputPassword, "邮箱验证成功") // 重新解析响应,检查下一步 json.Unmarshal(body, &data) } // 基于 get_code.go 的增强逻辑:检查是否有 continue_url // 如果有,直接跟随重定向获取授权码,跳过工作区选择 if cu, ok := data["continue_url"].(string); ok && cu != "" && !strings.Contains(cu, "email-verification") { if strings.Contains(cu, "consent") { // 访问 consent 页面 c.logStep(StepSelectWorkspace, "处理 consent 页面...") c.doRequest("GET", cu, nil, headers) } else { // 直接跟随重定向获取授权码 c.logStep(StepWaitCallback, "跟随 continue_url 重定向...") return c.followRedirectsForCode(cu, headers) } } } else { c.logStep(StepInputPassword, "跳过密码验证步骤 (服务器未要求)") } // 6. 选择工作区 c.logStep(StepSelectWorkspace, "选择工作区: %s", c.workspaceID) // 根据前面的流程选择正确的 Sentinel 请求 if !c.callSentinelReq("email_otp_validate__auto") { // 如果 email_otp_validate__auto 失败,尝试 password_verify__auto if !c.callSentinelReq("password_verify__auto") { return "", fmt.Errorf("Sentinel 请求失败") } } // 选择工作区时带上 Sentinel Header workspaceHeaders := make(map[string]string) for k, v := range headers { workspaceHeaders[k] = v } workspaceHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("workspace_select") workspacePayload := map[string]string{ "workspace_id": c.workspaceID, } // 添加 500 错误重试机制 - 最多重试 5 次,指数退避 + 随机抖动 var lastErr error for retry := 0; retry < 5; retry++ { if retry > 0 { // 指数退避: 3s, 5s, 8s, 12s 基础延迟 + 0~3s 随机抖动 baseDelay := time.Duration(3+retry*2) * time.Second jitter := time.Duration(rand.Intn(3000)) * time.Millisecond delay := baseDelay + jitter c.logStep(StepSelectWorkspace, "第 %d 次重试选择工作区 (等待 %.1fs)...", retry+1, delay.Seconds()) time.Sleep(delay) // 重新获取 Sentinel token if !c.callSentinelReq("password_verify__auto") { c.callSentinelReq("email_otp_validate__auto") } workspaceHeaders["OpenAI-Sentinel-Token"] = c.getSentinelHeader("workspace_select") } resp, body, err = c.doRequest("POST", "https://auth.openai.com/api/accounts/workspace/select", workspacePayload, workspaceHeaders) if err != nil { lastErr = fmt.Errorf("请求失败: %v", err) continue } // 成功 if resp.StatusCode == 200 { json.Unmarshal(body, &data) continueURL, ok := data["continue_url"].(string) if !ok || continueURL == "" { c.logError(StepSelectWorkspace, "未获取到 continue_url, 响应: %s", string(body[:min(500, len(body))])) return "", fmt.Errorf("未获取到 continue_url") } // 7. 跟随重定向获取授权码 c.logStep(StepWaitCallback, "跟随重定向...") for i := 0; i < 10; i++ { resp, _, err = c.doRequest("GET", continueURL, nil, headers) if err != nil { break } if resp.StatusCode >= 300 && resp.StatusCode < 400 { location := resp.Header.Get("Location") 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("未能获取授权码") } // 429 限流,可重试 if resp.StatusCode == 429 { c.logStep(StepSelectWorkspace, "请求限流 429,将重试...") lastErr = fmt.Errorf("请求限流: 429") continue } // 5xx 服务器错误,可重试 if resp.StatusCode >= 500 && resp.StatusCode < 600 { c.logStep(StepSelectWorkspace, "服务器错误 %d,将重试...", resp.StatusCode) lastErr = fmt.Errorf("服务器错误: %d", resp.StatusCode) continue } // 其他错误,不重试 c.logError(StepSelectWorkspace, "选择工作区失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))])) return "", fmt.Errorf("选择工作区失败: %d", resp.StatusCode) } // 重试耗尽 c.logError(StepSelectWorkspace, "选择工作区失败,重试已耗尽: %v", lastErr) return "", fmt.Errorf("选择工作区失败 (重试已耗尽): %v", lastErr) } // 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, } headers := map[string]string{"Content-Type": "application/json"} resp, body, err := c.doRequest("POST", "https://auth.openai.com/oauth/token", payload, headers) if err != nil { return nil, fmt.Errorf("token 交换失败: %v", err) } if resp.StatusCode != 200 { return nil, fmt.Errorf("token 交换失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))])) } var tokens CodexTokens if err := json.Unmarshal(body, &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 } // followRedirectsForCode 跟随重定向获取授权码 // 基于 get_code.go 的实现,用于复用重定向逻辑 func (c *CodexAPIAuth) followRedirectsForCode(continueURL string, headers map[string]string) (string, error) { c.logStep(StepWaitCallback, "跟随重定向获取授权码...") for i := 0; i < 10; i++ { resp, _, err := c.doRequest("GET", continueURL, nil, headers) if err != nil { break } if resp.StatusCode >= 300 && resp.StatusCode < 400 { location := resp.Header.Get("Location") 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("未能获取授权码") } // min 返回较小值 func min(a, b int) int { if a < b { return a } return b } // CompleteWithCodexAPI 使用纯 API 方式完成授权(带 403 重试换指纹机制) // authURL 和 sessionID 由 S2A 生成 func CompleteWithCodexAPI(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (string, error) { if logger != nil { if proxy != "" { logger.LogStep(StepBrowserStart, "使用 CodexAuth API 模式 (代理: %s)", proxy) } else { logger.LogStep(StepBrowserStart, "使用 CodexAuth API 模式 (无代理)") } } // 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) } 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) }