Files
codexautopool/backend/internal/auth/rod.go

261 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
// 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) {
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")
// 使用系统 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,
}, 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) {
// 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()
// 增加超时时间到 90 秒
page = page.Timeout(90 * time.Second)
if err := page.Navigate(authURL); err != nil {
return "", fmt.Errorf("访问授权URL失败: %v", err)
}
page.MustWaitDOMStable()
if code := r.checkForCode(page); code != "" {
return code, nil
}
emailInput, err := page.Timeout(5 * time.Second).Element("input[name='email'], input[type='email'], input[name='username']")
if err != nil {
return "", fmt.Errorf("未找到邮箱输入框")
}
emailInput.MustSelectAllText().MustInput(email)
time.Sleep(200 * time.Millisecond)
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
btn.MustClick()
}
time.Sleep(1500 * time.Millisecond)
if code := r.checkForCode(page); code != "" {
return code, nil
}
passwordInput, err := page.Timeout(8 * time.Second).Element("input[type='password']")
if err != nil {
return "", fmt.Errorf("未找到密码输入框")
}
passwordInput.MustSelectAllText().MustInput(password)
time.Sleep(200 * time.Millisecond)
if btn, _ := page.Timeout(2 * time.Second).Element("button[type='submit']"); btn != nil {
btn.MustClick()
}
for i := 0; i < 66; i++ {
time.Sleep(300 * time.Millisecond)
if code := r.checkForCode(page); code != "" {
return code, nil
}
info, _ := page.Info()
currentURL := info.URL
if strings.Contains(currentURL, "consent") {
if btn, _ := page.Timeout(500 * time.Millisecond).Element("button[type='submit']"); btn != nil {
btn.Click(proto.InputMouseButtonLeft, 1)
}
}
if strings.Contains(currentURL, "authorize") && teamID != "" {
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)
}
}
}
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) {
auth, err := NewRodAuth(headless, proxy)
if err != nil {
return "", err
}
defer auth.Close()
return auth.CompleteOAuth(authURL, email, password, teamID)
}
// CompleteWithBrowser 使用 Rod 完成 S2A 授权 (别名)
func CompleteWithBrowser(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
return CompleteWithRod(authURL, email, password, teamID, headless, proxy)
}