497 lines
14 KiB
Go
497 lines
14 KiB
Go
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,
|
||
"canShare−function 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", "application/json")
|
||
|
||
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...)
|
||
}
|
||
}
|
||
|
||
// 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))
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("Origin", "https://auth.openai.com")
|
||
req.Header.Set("Referer", 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))
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("Origin", "https://auth.openai.com")
|
||
req.Header.Set("Referer", 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))
|
||
req.Header.Set("Content-Type", "application/json")
|
||
req.Header.Set("Origin", "https://auth.openai.com")
|
||
req.Header.Set("Referer", referer)
|
||
|
||
resp, err = c.client.Do(req)
|
||
if err != nil {
|
||
c.logError(StepSelectWorkspace, "选择工作区失败: %v", err)
|
||
return "", fmt.Errorf("选择工作区失败: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
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
|
||
}
|