259 lines
6.6 KiB
Go
259 lines
6.6 KiB
Go
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.
|
||
if r.proxy != "" {
|
||
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 = page.Timeout(45 * 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)
|
||
}
|