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" utls "github.com/refraction-networking/utls" "golang.org/x/net/http2" ) func init() { rand.Seed(time.Now().UnixNano()) } // 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义 // CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现 type CodexAPIAuth struct { client *http.Client email string password string workspaceID string authURL string // S2A 生成的授权 URL sessionID string // S2A 会话 ID deviceID string sid string sentinelToken string solvedPow string userAgent 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\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 实例 // 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) if err != nil { 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{ client: client, email: email, password: password, workspaceID: workspaceID, authURL: authURL, 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", 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 { 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 { 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 请求 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 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) 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.solvedPow = initToken } } else { 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 获取授权码 func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) { c.logStep(StepNavigate, "开始 Codex API 授权流程...") // 使用 S2A 生成的授权 URL(不再自己生成 PKCE 参数) if c.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", c.authURL, nil, headers) if err != nil { c.logError(StepNavigate, "访问授权页失败: %v", err) return "", fmt.Errorf("访问授权页失败: %v", err) } // 手动跟随重定向 currentURL := c.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, "邮箱提交响应 pageType=%s, 包含password=%v", pageType, strings.Contains(string(body), "password")) 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, "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, "密码验证成功") } else { c.logStep(StepInputPassword, "跳过密码验证步骤 (服务器未要求)") } // 6. 选择工作区 c.logStep(StepSelectWorkspace, "选择工作区: %s", c.workspaceID) if !c.callSentinelReq("password_verify__auto") { return "", fmt.Errorf("Sentinel 请求失败") } workspacePayload := map[string]string{ "workspace_id": c.workspaceID, } resp, body, err = c.doRequest("POST", "https://auth.openai.com/api/accounts/workspace/select", workspacePayload, headers) if err != nil || resp.StatusCode != 200 { c.logError(StepSelectWorkspace, "选择工作区失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))])) return "", fmt.Errorf("选择工作区失败: %d", resp.StatusCode) } 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("未能获取授权码") } // 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 } // min 返回较小值 func min(a, b int) int { if a < b { return a } return b } // CompleteWithCodexAPI 使用纯 API 方式完成授权 // authURL 和 sessionID 由 S2A 生成 func CompleteWithCodexAPI(email, password, workspaceID, authURL, sessionID, proxy string, logger *AuthLogger) (string, error) { if logger != nil { logger.LogStep(StepBrowserStart, "使用 CodexAuth API 模式...") } auth, err := NewCodexAPIAuth(email, password, workspaceID, authURL, sessionID, 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 }