feat: Add a new Go backend module for API-based authentication, featuring TLS fingerprinting and Proof-of-Work solving.
This commit is contained in:
@@ -2,24 +2,30 @@ package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codex-pool/internal/client"
|
||||
utls "github.com/refraction-networking/utls"
|
||||
"golang.org/x/net/http2"
|
||||
)
|
||||
|
||||
// 常量 CodexClientID, CodexRedirectURI, CodexScope 已在 s2a.go 中定义
|
||||
|
||||
// CodexAPIAuth 纯 API 授权 (无浏览器)
|
||||
// CodexAPIAuth 纯 API 授权 (无浏览器) - 基于 get_code.go 的实现
|
||||
type CodexAPIAuth struct {
|
||||
client *client.TLSClient
|
||||
client *http.Client
|
||||
email string
|
||||
password string
|
||||
workspaceID string
|
||||
@@ -28,24 +34,126 @@ type CodexAPIAuth struct {
|
||||
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 实例
|
||||
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)
|
||||
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: tlsClient,
|
||||
client: client,
|
||||
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",
|
||||
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
|
||||
}
|
||||
@@ -54,9 +162,8 @@ func NewCodexAPIAuth(email, password, workspaceID, proxy string, logger *AuthLog
|
||||
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:])
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||
}
|
||||
|
||||
// generateCodeVerifier 生成 PKCE code_verifier
|
||||
@@ -96,14 +203,19 @@ func fnv1a32(data []byte) uint32 {
|
||||
|
||||
// getParseTime 生成 JS Date().toString() 格式的时间字符串
|
||||
func getParseTime() string {
|
||||
now := time.Now()
|
||||
return now.Format("Mon Jan 02 2006 15:04:05") + " GMT+0800 (中国标准时间)"
|
||||
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{}{
|
||||
2500 + rand.Intn(1000),
|
||||
rand.Intn(1000) + 2500,
|
||||
getParseTime(),
|
||||
4294967296,
|
||||
0,
|
||||
@@ -114,13 +226,13 @@ func (c *CodexAPIAuth) getConfig() []interface{} {
|
||||
"zh-CN",
|
||||
0,
|
||||
"canShare−function canShare() { [native code] }",
|
||||
fmt.Sprintf("_reactListening%d", 1000000+rand.Intn(9000000)),
|
||||
fmt.Sprintf("_reactListening%d", rand.Intn(9000000)+1000000),
|
||||
"onhashchange",
|
||||
float64(time.Now().UnixNano()/1e6) / 1000.0,
|
||||
float64(time.Now().UnixNano()) / 1e6,
|
||||
c.sid,
|
||||
"",
|
||||
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)
|
||||
encoded := base64.StdEncoding.EncodeToString(jsonStr)
|
||||
|
||||
h := fnv1a32(append(seedBytes, []byte(encoded)...))
|
||||
combined := append(seedBytes, []byte(encoded)...)
|
||||
h := fnv1a32(combined)
|
||||
hexHash := fmt.Sprintf("%08x", h)
|
||||
|
||||
if hexHash[:len(difficulty)] <= difficulty {
|
||||
@@ -156,75 +269,100 @@ func (c *CodexAPIAuth) getRequirementsToken() string {
|
||||
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) error {
|
||||
func (c *CodexAPIAuth) callSentinelReq(flow string) bool {
|
||||
initToken := c.getRequirementsToken()
|
||||
payload := map[string]interface{}{
|
||||
payload := map[string]string{
|
||||
"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)
|
||||
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
|
||||
}
|
||||
|
||||
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"`
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal(body, &data)
|
||||
|
||||
if token, ok := data["token"].(string); ok {
|
||||
c.sentinelToken = token
|
||||
}
|
||||
|
||||
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 解决失败")
|
||||
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
|
||||
}
|
||||
c.solvedPow = "gAAAAAB" + solved
|
||||
} else {
|
||||
c.solvedPow = initToken
|
||||
}
|
||||
|
||||
return nil
|
||||
return true
|
||||
}
|
||||
|
||||
// getSentinelHeader 构建 sentinel header
|
||||
func (c *CodexAPIAuth) getSentinelHeader(flow string) string {
|
||||
obj := map[string]interface{}{
|
||||
sentinelObj := map[string]string{
|
||||
"p": c.solvedPow,
|
||||
"id": c.deviceID,
|
||||
"flow": flow,
|
||||
}
|
||||
if c.sentinelToken != "" {
|
||||
obj["c"] = c.sentinelToken
|
||||
sentinelObj["c"] = c.sentinelToken
|
||||
}
|
||||
header, _ := json.Marshal(obj)
|
||||
header, _ := json.Marshal(sentinelObj)
|
||||
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 获取授权码
|
||||
func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
|
||||
c.logStep(StepNavigate, "开始 Codex API 授权流程...")
|
||||
@@ -278,158 +405,134 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
|
||||
|
||||
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, "访问授权页面...")
|
||||
req, _ := http.NewRequest("GET", authURL, nil)
|
||||
req.Header.Set("Referer", "https://auth.openai.com/log-in")
|
||||
resp, err := c.client.Do(req)
|
||||
resp, _, err := c.doRequest("GET", authURL, nil, headers)
|
||||
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()
|
||||
|
||||
// 手动跟随重定向
|
||||
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. 提交邮箱
|
||||
c.logStep(StepInputEmail, "提交邮箱: %s", c.email)
|
||||
if err := c.callSentinelReq("login_web_init"); err != nil {
|
||||
c.logError(StepInputEmail, "Sentinel 请求失败: %v", err)
|
||||
return "", err
|
||||
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,
|
||||
},
|
||||
}
|
||||
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))
|
||||
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)
|
||||
}
|
||||
|
||||
// 解析响应,检查是否需要密码
|
||||
emailResp, _ := client.ReadBody(resp)
|
||||
var emailResult map[string]interface{}
|
||||
json.Unmarshal(emailResp, &emailResult)
|
||||
var data map[string]interface{}
|
||||
json.Unmarshal(body, &data)
|
||||
|
||||
// 5. 验证密码
|
||||
c.logStep(StepInputPassword, "验证密码...")
|
||||
if err := c.callSentinelReq("authorize_continue__auto"); err != nil {
|
||||
c.logError(StepInputPassword, "Sentinel 请求失败: %v", err)
|
||||
return "", err
|
||||
// 检查是否需要密码验证
|
||||
pageType := ""
|
||||
if page, ok := data["page"].(map[string]interface{}); ok {
|
||||
if pt, ok := page["type"].(string); ok {
|
||||
pageType = pt
|
||||
}
|
||||
}
|
||||
|
||||
pwdPayload := map[string]string{
|
||||
"username": c.email,
|
||||
"password": c.password,
|
||||
}
|
||||
pwdBody, _ := json.Marshal(pwdPayload)
|
||||
if pageType == "password" || strings.Contains(string(body), "password") {
|
||||
// 5. 验证密码
|
||||
c.logStep(StepInputPassword, "验证密码...")
|
||||
if !c.callSentinelReq("authorize_continue__auto") {
|
||||
return "", fmt.Errorf("Sentinel 请求失败")
|
||||
}
|
||||
|
||||
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"))
|
||||
verifyHeaders := make(map[string]string)
|
||||
for k, v := range headers {
|
||||
verifyHeaders[k] = v
|
||||
}
|
||||
verifyHeaders["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()
|
||||
passwordPayload := map[string]string{
|
||||
"username": c.email,
|
||||
"password": c.password,
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := client.ReadBody(resp)
|
||||
c.logError(StepInputPassword, "密码验证失败: %d - %s", resp.StatusCode, string(body))
|
||||
return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 选择工作区
|
||||
c.logStep(StepSelectWorkspace, "选择工作区: %s", c.workspaceID)
|
||||
if err := c.callSentinelReq("password_verify__auto"); err != nil {
|
||||
c.logError(StepSelectWorkspace, "Sentinel 请求失败: %v", err)
|
||||
return "", err
|
||||
if !c.callSentinelReq("password_verify__auto") {
|
||||
return "", fmt.Errorf("Sentinel 请求失败")
|
||||
}
|
||||
|
||||
wsPayload := map[string]string{
|
||||
workspacePayload := 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))
|
||||
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)
|
||||
}
|
||||
|
||||
// 解析 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)
|
||||
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, "跟随重定向...")
|
||||
continueURL := wsResult.ContinueURL
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
req, _ = http.NewRequest("GET", continueURL, nil)
|
||||
resp, err = c.client.Do(req)
|
||||
resp, _, err = c.doRequest("GET", continueURL, nil, headers)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
location := resp.Header.Get("Location")
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 301 && resp.StatusCode <= 308 {
|
||||
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, "授权成功,获取到授权码")
|
||||
@@ -456,24 +559,18 @@ func (c *CodexAPIAuth) ExchangeCodeForTokens(code, codeVerifier string) (*CodexT
|
||||
"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)
|
||||
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)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
respBody, _ := client.ReadBody(resp)
|
||||
return nil, fmt.Errorf("token 交换失败: %d - %s", resp.StatusCode, string(respBody))
|
||||
return nil, fmt.Errorf("token 交换失败: %d - %s", resp.StatusCode, string(body[:min(200, len(body))]))
|
||||
}
|
||||
|
||||
respBody, _ := client.ReadBody(resp)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -485,7 +582,14 @@ func (c *CodexAPIAuth) ExchangeCodeForTokens(code, codeVerifier string) (*CodexT
|
||||
return &tokens, nil
|
||||
}
|
||||
|
||||
// CodexTokens 结构体已在 s2a.go 中定义
|
||||
// min 返回较小值
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// CompleteWithCodexAPI 使用纯 API 方式完成授权
|
||||
func CompleteWithCodexAPI(email, password, workspaceID, proxy string, logger *AuthLogger) (string, error) {
|
||||
if logger != nil {
|
||||
|
||||
Reference in New Issue
Block a user