feat: Add automated ChatGPT account registration with backend API, TLS client, and fingerprinting, alongside new frontend pages for configuration, monitoring, and upload.
This commit is contained in:
@@ -75,8 +75,18 @@ func (rt *utlsRoundTripper) RoundTrip(req *http.Request) (*http.Response, error)
|
||||
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)
|
||||
// 构建 CONNECT 请求,支持代理认证
|
||||
connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n", targetHost, targetHost)
|
||||
|
||||
// 添加代理认证头
|
||||
if rt.proxyURL.User != nil {
|
||||
username := rt.proxyURL.User.Username()
|
||||
password, _ := rt.proxyURL.User.Password()
|
||||
auth := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
|
||||
connectReq += fmt.Sprintf("Proxy-Authorization: Basic %s\r\n", auth)
|
||||
}
|
||||
connectReq += "\r\n"
|
||||
|
||||
_, err = conn.Write([]byte(connectReq))
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
@@ -493,10 +503,10 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
|
||||
return "", fmt.Errorf("密码验证失败: %d", resp.StatusCode)
|
||||
}
|
||||
c.logStep(StepInputPassword, "密码验证成功")
|
||||
|
||||
|
||||
// 解析密码验证响应
|
||||
json.Unmarshal(body, &data)
|
||||
|
||||
|
||||
// 检查下一步是什么
|
||||
nextPageType := ""
|
||||
if page, ok := data["page"].(map[string]interface{}); ok {
|
||||
@@ -504,7 +514,7 @@ func (c *CodexAPIAuth) ObtainAuthorizationCode() (string, error) {
|
||||
nextPageType = pt
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 如果需要邮箱验证,这是新账号的问题
|
||||
if nextPageType == "email_otp_verification" {
|
||||
c.logError(StepInputPassword, "账号需要邮箱验证,无法继续 Codex 授权流程")
|
||||
|
||||
@@ -1,377 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codex-pool/internal/proxyutil"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/launcher"
|
||||
"github.com/go-rod/rod/lib/proto"
|
||||
"github.com/go-rod/stealth"
|
||||
)
|
||||
|
||||
// RodAuth 使用 Rod + Stealth 完成 OAuth 授权
|
||||
type RodAuth struct {
|
||||
browser *rod.Browser
|
||||
headless bool
|
||||
proxy string
|
||||
proxyUser string
|
||||
proxyPass string
|
||||
profile BrowserProfile // 随机浏览器配置
|
||||
}
|
||||
|
||||
// getChromiumPath 获取 Chromium 路径
|
||||
func getChromiumPath() string {
|
||||
// 优先使用环境变量
|
||||
if path := os.Getenv("CHROME_BIN"); path != "" {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
if path := os.Getenv("CHROME_PATH"); path != "" {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
// Alpine Linux 默认路径
|
||||
paths := []string{
|
||||
"/usr/bin/chromium-browser",
|
||||
"/usr/bin/chromium",
|
||||
"/usr/bin/google-chrome",
|
||||
"/usr/bin/google-chrome-stable",
|
||||
}
|
||||
for _, path := range paths {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
}
|
||||
return "" // 让 Rod 自动下载
|
||||
}
|
||||
|
||||
// NewRodAuth 创建 Rod 授权器
|
||||
func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
|
||||
// 获取随机浏览器配置
|
||||
profile := GetRandomBrowserProfile()
|
||||
|
||||
var proxyServer string
|
||||
var proxyUser string
|
||||
var proxyPass string
|
||||
if proxy != "" {
|
||||
info, err := proxyutil.Parse(proxy)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("代理格式错误: %v", err)
|
||||
}
|
||||
if info.URL != nil {
|
||||
proxy = info.URL.String() // normalized
|
||||
}
|
||||
if info.Server != nil {
|
||||
proxyServer = info.Server.String()
|
||||
}
|
||||
proxyUser = info.Username
|
||||
proxyPass = info.Password
|
||||
}
|
||||
|
||||
l := launcher.New().
|
||||
Headless(headless).
|
||||
Set("disable-blink-features", "AutomationControlled").
|
||||
Set("disable-dev-shm-usage").
|
||||
Set("no-sandbox").
|
||||
Set("disable-gpu").
|
||||
Set("disable-extensions").
|
||||
Set("disable-background-networking").
|
||||
Set("disable-sync").
|
||||
Set("disable-translate").
|
||||
Set("metrics-recording-only").
|
||||
Set("no-first-run").
|
||||
Set("disable-infobars").
|
||||
Set("disable-automation").
|
||||
// 使用随机语言和窗口大小
|
||||
Set("lang", strings.Split(profile.AcceptLang, ",")[0]).
|
||||
Set("window-size", fmt.Sprintf("%d,%d", profile.Width, profile.Height)).
|
||||
// 随机 User-Agent
|
||||
UserDataDir("").
|
||||
Set("user-agent", profile.UserAgent)
|
||||
|
||||
// 使用系统 Chromium(如果存在)
|
||||
if chromiumPath := getChromiumPath(); chromiumPath != "" {
|
||||
l = l.Bin(chromiumPath)
|
||||
}
|
||||
|
||||
if proxyServer != "" {
|
||||
l = l.Proxy(proxyServer)
|
||||
}
|
||||
|
||||
controlURL, err := l.Launch()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("启动浏览器失败: %v", err)
|
||||
}
|
||||
|
||||
browser := rod.New().ControlURL(controlURL)
|
||||
if err := browser.Connect(); err != nil {
|
||||
return nil, fmt.Errorf("连接浏览器失败: %v", err)
|
||||
}
|
||||
|
||||
return &RodAuth{
|
||||
browser: browser,
|
||||
headless: headless,
|
||||
proxy: proxy,
|
||||
proxyUser: proxyUser,
|
||||
proxyPass: proxyPass,
|
||||
profile: profile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close 关闭浏览器
|
||||
func (r *RodAuth) Close() {
|
||||
if r.browser != nil {
|
||||
r.browser.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// CompleteOAuth 完成 OAuth 授权
|
||||
func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string, error) {
|
||||
return r.CompleteOAuthLogged(authURL, email, password, teamID, nil)
|
||||
}
|
||||
|
||||
// CompleteOAuthLogged 完成 OAuth 授权(带日志回调)
|
||||
func (r *RodAuth) CompleteOAuthLogged(authURL, email, password, teamID string, logger *AuthLogger) (string, error) {
|
||||
// 日志辅助函数
|
||||
logStep := func(step AuthStep, format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.LogStep(step, format, args...)
|
||||
}
|
||||
}
|
||||
logError := func(step AuthStep, format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.LogError(step, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle proxy auth (407) in headless mode.
|
||||
// When Fetch domain is enabled without patterns, requests will be paused and must be continued.
|
||||
// 只在代理需要认证时才启用 Fetch 域
|
||||
if r.proxy != "" && r.proxyUser != "" {
|
||||
authBrowser, cancel := r.browser.WithCancel()
|
||||
defer cancel()
|
||||
|
||||
restoreFetch := authBrowser.EnableDomain("", &proto.FetchEnable{HandleAuthRequests: true})
|
||||
defer restoreFetch()
|
||||
|
||||
wait := authBrowser.EachEvent(
|
||||
func(e *proto.FetchRequestPaused) {
|
||||
_ = proto.FetchContinueRequest{RequestID: e.RequestID}.Call(authBrowser)
|
||||
},
|
||||
func(e *proto.FetchAuthRequired) {
|
||||
resp := &proto.FetchAuthChallengeResponse{
|
||||
Response: proto.FetchAuthChallengeResponseResponseDefault,
|
||||
}
|
||||
if e.AuthChallenge != nil && e.AuthChallenge.Source == proto.FetchAuthChallengeSourceProxy {
|
||||
if r.proxyUser != "" {
|
||||
resp.Response = proto.FetchAuthChallengeResponseResponseProvideCredentials
|
||||
resp.Username = r.proxyUser
|
||||
resp.Password = r.proxyPass
|
||||
} else {
|
||||
// Fail fast if the proxy requires auth but user didn't provide credentials.
|
||||
resp.Response = proto.FetchAuthChallengeResponseResponseCancelAuth
|
||||
}
|
||||
}
|
||||
_ = proto.FetchContinueWithAuth{RequestID: e.RequestID, AuthChallengeResponse: resp}.Call(authBrowser)
|
||||
},
|
||||
)
|
||||
go wait()
|
||||
}
|
||||
|
||||
page, err := stealth.Page(r.browser)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建页面失败: %v", err)
|
||||
}
|
||||
defer page.Close()
|
||||
|
||||
// 设置随机窗口大小
|
||||
_ = page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
|
||||
Width: r.profile.Width,
|
||||
Height: r.profile.Height,
|
||||
DeviceScaleFactor: r.profile.PixelRatio,
|
||||
Mobile: false,
|
||||
})
|
||||
|
||||
// 注入额外的反检测脚本
|
||||
antiDetectionJS := GetAntiDetectionJS(r.profile)
|
||||
_, _ = page.Evaluate(&rod.EvalOptions{
|
||||
JS: antiDetectionJS,
|
||||
ByValue: true,
|
||||
AwaitPromise: false,
|
||||
ThisObj: nil,
|
||||
})
|
||||
|
||||
// 设置合理的超时时间 60 秒
|
||||
page = page.Timeout(60 * time.Second)
|
||||
|
||||
if err := page.Navigate(authURL); err != nil {
|
||||
logError(StepNavigate, "访问授权页失败: %v", err)
|
||||
return "", fmt.Errorf("访问授权URL失败: %v", err)
|
||||
}
|
||||
|
||||
page.MustWaitDOMStable()
|
||||
|
||||
// 获取当前URL
|
||||
info, _ := page.Info()
|
||||
currentURL := info.URL
|
||||
logStep(StepNavigate, "页面加载完成 | URL: %s", currentURL)
|
||||
|
||||
if code := r.checkForCode(page); code != "" {
|
||||
logStep(StepComplete, "授权成功(快速通道)")
|
||||
return code, nil
|
||||
}
|
||||
|
||||
// 使用10秒超时查找邮箱输入框
|
||||
emailInput, err := page.Timeout(10 * time.Second).Element("input[name='email'], input[type='email'], input[name='username'], input[id='email'], input[autocomplete='email']")
|
||||
if err != nil {
|
||||
info, _ := page.Info()
|
||||
logError(StepInputEmail, "未找到邮箱输入框 | URL: %s", info.URL)
|
||||
return "", fmt.Errorf("未找到邮箱输入框")
|
||||
}
|
||||
|
||||
logStep(StepInputEmail, "邮箱已填写")
|
||||
emailInput.MustSelectAllText().MustInput(email)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
// 点击提交按钮
|
||||
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit'], div._ctas_1alro_13 button, button[name='action']"); btn != nil {
|
||||
btn.MustClick()
|
||||
}
|
||||
|
||||
// 等待页面跳转(等待 URL 变化或密码框出现,最多 10 秒)
|
||||
passwordSelector := "input[name='current-password'], input[autocomplete='current-password'], input[type='password'], input[name='password'], input[id='password']"
|
||||
var passwordFound bool
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if code := r.checkForCode(page); code != "" {
|
||||
logStep(StepComplete, "授权成功")
|
||||
return code, nil
|
||||
}
|
||||
|
||||
info, _ = page.Info()
|
||||
// 检查是否有密码输入框(页面已跳转)
|
||||
if pwdInput, _ := page.Timeout(100 * time.Millisecond).Element(passwordSelector); pwdInput != nil {
|
||||
passwordFound = true
|
||||
break
|
||||
}
|
||||
|
||||
// 如果 URL 已经变化(包含 password),也跳出
|
||||
if strings.Contains(info.URL, "password") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前URL用于调试
|
||||
info, _ = page.Info()
|
||||
logStep(StepInputPassword, "查找密码框 | URL: %s", info.URL)
|
||||
|
||||
// 使用10秒超时查找密码输入框(优先使用 current-password)
|
||||
var passwordInput *rod.Element
|
||||
if passwordFound {
|
||||
passwordInput, err = page.Timeout(2 * time.Second).Element(passwordSelector)
|
||||
} else {
|
||||
passwordInput, err = page.Timeout(10 * time.Second).Element(passwordSelector)
|
||||
}
|
||||
if err != nil {
|
||||
info, _ := page.Info()
|
||||
logError(StepInputPassword, "未找到密码输入框 | URL: %s", info.URL)
|
||||
return "", fmt.Errorf("未找到密码输入框")
|
||||
}
|
||||
|
||||
logStep(StepInputPassword, "密码已填写")
|
||||
passwordInput.MustSelectAllText().MustInput(password)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
|
||||
logStep(StepSubmitPassword, "正在登录...")
|
||||
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit'], div._ctas_1alro_13 button, button[name='action']"); btn != nil {
|
||||
btn.MustClick()
|
||||
}
|
||||
|
||||
// 等待授权回调(最多20秒)
|
||||
for i := 0; i < 40; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
if code := r.checkForCode(page); code != "" {
|
||||
logStep(StepComplete, "授权成功")
|
||||
return code, nil
|
||||
}
|
||||
|
||||
info, _ := page.Info()
|
||||
currentURL := info.URL
|
||||
|
||||
if strings.Contains(currentURL, "consent") {
|
||||
logStep(StepConsent, "处理授权同意... | URL: %s", currentURL)
|
||||
// 同意页面的确认按钮(第二个按钮)
|
||||
if btn, _ := page.Timeout(500 * time.Millisecond).Element("div._ctas_1alro_13 div:nth-child(2) button, button[type='submit'], div._ctas_1alro_13 button"); btn != nil {
|
||||
btn.Click(proto.InputMouseButtonLeft, 1)
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(currentURL, "authorize") && teamID != "" {
|
||||
logStep(StepSelectWorkspace, "选择工作区...")
|
||||
wsSelector := fmt.Sprintf("[data-workspace-id='%s'], [data-account-id='%s']", teamID, teamID)
|
||||
if wsBtn, _ := page.Timeout(500 * time.Millisecond).Element(wsSelector); wsBtn != nil {
|
||||
wsBtn.Click(proto.InputMouseButtonLeft, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logError(StepWaitCallback, "授权超时")
|
||||
return "", fmt.Errorf("授权超时")
|
||||
}
|
||||
|
||||
// checkForCode 检查 URL 中是否包含 code
|
||||
func (r *RodAuth) checkForCode(page *rod.Page) string {
|
||||
info, err := page.Info()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(info.URL, "code=") {
|
||||
return ExtractCodeFromCallbackURL(info.URL)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// CompleteWithRod 使用 Rod + Stealth 完成 S2A 授权
|
||||
func CompleteWithRod(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
|
||||
return CompleteWithRodLogged(authURL, email, password, teamID, headless, proxy, nil)
|
||||
}
|
||||
|
||||
// CompleteWithRodLogged 使用 Rod + Stealth 完成 S2A 授权(带日志回调)
|
||||
func CompleteWithRodLogged(authURL, email, password, teamID string, headless bool, proxy string, logger *AuthLogger) (string, error) {
|
||||
// 日志辅助函数
|
||||
logStep := func(step AuthStep, format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.LogStep(step, format, args...)
|
||||
}
|
||||
}
|
||||
logError := func(step AuthStep, format string, args ...interface{}) {
|
||||
if logger != nil {
|
||||
logger.LogError(step, format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
logStep(StepBrowserStart, "正在启动 Rod 浏览器...")
|
||||
auth, err := NewRodAuth(headless, proxy)
|
||||
if err != nil {
|
||||
logError(StepBrowserStart, "启动失败: %v", err)
|
||||
return "", err
|
||||
}
|
||||
defer auth.Close()
|
||||
|
||||
logStep(StepBrowserStart, "浏览器启动成功")
|
||||
return auth.CompleteOAuthLogged(authURL, email, password, teamID, logger)
|
||||
}
|
||||
|
||||
// CompleteWithBrowser 使用 Rod 完成 S2A 授权 (别名)
|
||||
func CompleteWithBrowser(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
|
||||
return CompleteWithRod(authURL, email, password, teamID, headless, proxy)
|
||||
}
|
||||
Reference in New Issue
Block a user