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

315 lines
8.7 KiB
Go

package auth
import (
"context"
"fmt"
"strings"
"time"
"codex-pool/internal/proxyutil"
"github.com/chromedp/cdproto/fetch"
"github.com/chromedp/cdproto/network"
"github.com/chromedp/cdproto/page"
"github.com/chromedp/chromedp"
)
// CompleteWithChromedp 使用 chromedp 完成 S2A OAuth 授权
func CompleteWithChromedp(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
return CompleteWithChromedpLogged(authURL, email, password, teamID, headless, proxy, nil)
}
// CompleteWithChromedpLogged 使用 chromedp 完成 S2A OAuth 授权(带日志回调)
func CompleteWithChromedpLogged(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, "正在启动 Chromedp 浏览器...")
// 获取随机浏览器配置
profile := GetRandomBrowserProfile()
var proxyServer string
var proxyUser string
var proxyPass string
if proxy != "" {
info, err := proxyutil.Parse(proxy)
if err != nil {
return "", fmt.Errorf("代理格式错误: %v", err)
}
if info.Server != nil {
proxyServer = info.Server.String()
}
proxyUser = info.Username
proxyPass = info.Password
}
opts := append(chromedp.DefaultExecAllocatorOptions[:],
chromedp.Flag("headless", headless),
chromedp.Flag("disable-gpu", true),
chromedp.Flag("no-sandbox", true),
chromedp.Flag("disable-dev-shm-usage", true),
chromedp.Flag("disable-blink-features", "AutomationControlled"),
chromedp.Flag("disable-automation", true),
chromedp.Flag("disable-extensions", true),
chromedp.Flag("disable-infobars", true),
chromedp.Flag("enable-features", "NetworkService,NetworkServiceInProcess"),
// 使用随机 User-Agent
chromedp.UserAgent(profile.UserAgent),
// 使用随机窗口大小
chromedp.WindowSize(profile.Width, profile.Height),
// 随机语言
chromedp.Flag("accept-lang", profile.AcceptLang),
)
if proxyServer != "" {
opts = append(opts, chromedp.ProxyServer(proxyServer))
}
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
defer cancel()
ctx, cancel := chromedp.NewContext(allocCtx)
defer cancel()
// 增加超时时间到 180 秒
ctx, cancel = context.WithTimeout(ctx, 180*time.Second)
defer cancel()
var callbackURL string
// 只在代理需要认证时才启用 Fetch 域
if proxyServer != "" && proxyUser != "" {
chromedp.ListenTarget(ctx, func(ev interface{}) {
switch ev := ev.(type) {
case *fetch.EventRequestPaused:
// Fetch domain pauses requests; we must continue them to avoid stalling navigation.
reqID := ev.RequestID
go func() { _ = fetch.ContinueRequest(reqID).Do(ctx) }()
case *fetch.EventAuthRequired:
reqID := ev.RequestID
source := fetch.AuthChallengeSourceServer
if ev.AuthChallenge != nil {
source = ev.AuthChallenge.Source
}
go func() {
resp := &fetch.AuthChallengeResponse{Response: fetch.AuthChallengeResponseResponseDefault}
if source == fetch.AuthChallengeSourceProxy {
if proxyUser != "" {
resp.Response = fetch.AuthChallengeResponseResponseProvideCredentials
resp.Username = proxyUser
resp.Password = proxyPass
} else {
// Fail fast if the proxy requires auth but user didn't provide credentials.
resp.Response = fetch.AuthChallengeResponseResponseCancelAuth
}
}
_ = fetch.ContinueWithAuth(reqID, resp).Do(ctx)
}()
}
})
}
// 监听回调 URL
chromedp.ListenTarget(ctx, func(ev interface{}) {
if ev, ok := ev.(*network.EventRequestWillBeSent); ok {
url := ev.Request.URL
if strings.Contains(url, "localhost") && strings.Contains(url, "code=") {
callbackURL = url
}
}
})
// 获取反检测脚本
antiDetectionJS := GetAntiDetectionJS(profile)
// 构建运行任务
tasks := []chromedp.Action{
network.Enable(),
// 在每个新文档加载时注入反检测脚本
chromedp.ActionFunc(func(ctx context.Context) error {
_, err := page.AddScriptToEvaluateOnNewDocument(antiDetectionJS).Do(ctx)
return err
}),
chromedp.Navigate(authURL),
chromedp.WaitReady("body"),
}
// 只在代理需要认证时才启用 Fetch 域
if proxyServer != "" && proxyUser != "" {
tasks = append([]chromedp.Action{fetch.Enable().WithHandleAuthRequests(true)}, tasks...)
}
logStep(StepNavigate, "正在访问授权页面...")
err := chromedp.Run(ctx, tasks...)
if err != nil {
logError(StepNavigate, "访问失败: %v", err)
return "", fmt.Errorf("访问失败: %v", err)
}
time.Sleep(2 * time.Second)
if callbackURL != "" {
logStep(StepExtractCode, "已捕获授权码回调")
return ExtractCodeFromCallbackURL(callbackURL), nil
}
var currentURL string
_ = chromedp.Run(ctx, chromedp.Location(&currentURL))
if strings.Contains(currentURL, "code=") {
return ExtractCodeFromCallbackURL(currentURL), nil
}
time.Sleep(1 * time.Second)
emailSelectors := []string{
`input[name="email"]`,
`input[type="email"]`,
`input[name="username"]`,
}
logStep(StepInputEmail, "正在查找邮箱输入框...")
var emailFilled bool
for _, sel := range emailSelectors {
err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery))
if err == nil {
err = chromedp.Run(ctx,
chromedp.Clear(sel, chromedp.ByQuery),
chromedp.SendKeys(sel, email, chromedp.ByQuery),
)
if err == nil {
emailFilled = true
logStep(StepInputEmail, "已输入邮箱")
break
}
}
}
if !emailFilled {
logError(StepInputEmail, "未找到邮箱输入框")
return "", fmt.Errorf("未找到邮箱输入框")
}
time.Sleep(300 * time.Millisecond)
buttonSelectors := []string{
`button[type="submit"]`,
`button[data-testid="login-button"]`,
`button.continue-btn`,
`input[type="submit"]`,
}
logStep(StepSubmitEmail, "正在提交邮箱...")
for _, sel := range buttonSelectors {
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
if err == nil {
break
}
}
time.Sleep(1500 * time.Millisecond)
if callbackURL != "" {
logStep(StepExtractCode, "已捕获授权码回调")
return ExtractCodeFromCallbackURL(callbackURL), nil
}
_ = chromedp.Run(ctx, chromedp.Location(&currentURL))
if strings.Contains(currentURL, "code=") {
logStep(StepExtractCode, "已获取授权码")
return ExtractCodeFromCallbackURL(currentURL), nil
}
logStep(StepInputPassword, "正在查找密码输入框...")
passwordSelectors := []string{
`input[name="current-password"]`,
`input[name="password"]`,
`input[type="password"]`,
}
var passwordFilled bool
for _, sel := range passwordSelectors {
err = chromedp.Run(ctx, chromedp.WaitVisible(sel, chromedp.ByQuery))
if err == nil {
err = chromedp.Run(ctx,
chromedp.Clear(sel, chromedp.ByQuery),
chromedp.SendKeys(sel, password, chromedp.ByQuery),
)
if err == nil {
passwordFilled = true
logStep(StepInputPassword, "已输入密码")
break
}
}
}
if !passwordFilled {
logError(StepInputPassword, "未找到密码输入框")
return "", fmt.Errorf("未找到密码输入框")
}
time.Sleep(300 * time.Millisecond)
logStep(StepSubmitPassword, "正在提交密码...")
for _, sel := range buttonSelectors {
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
if err == nil {
break
}
}
logStep(StepWaitCallback, "等待授权回调...")
for i := 0; i < 30; i++ {
time.Sleep(500 * time.Millisecond)
if callbackURL != "" {
return ExtractCodeFromCallbackURL(callbackURL), nil
}
var url string
if err := chromedp.Run(ctx, chromedp.Location(&url)); err == nil {
if strings.Contains(url, "code=") {
logStep(StepExtractCode, "已获取授权码")
return ExtractCodeFromCallbackURL(url), nil
}
if strings.Contains(url, "consent") {
logStep(StepConsent, "正在处理授权同意页面...")
for _, sel := range buttonSelectors {
err = chromedp.Run(ctx, chromedp.Click(sel, chromedp.ByQuery))
if err == nil {
break
}
}
time.Sleep(1 * time.Second)
}
if strings.Contains(url, "authorize") && teamID != "" {
logStep(StepSelectWorkspace, "正在选择工作区: %s", teamID)
err = chromedp.Run(ctx,
chromedp.Click(fmt.Sprintf(`[data-workspace-id="%s"], [data-account-id="%s"]`, teamID, teamID), chromedp.ByQuery),
)
}
}
}
if callbackURL != "" {
logStep(StepComplete, "授权完成")
return ExtractCodeFromCallbackURL(callbackURL), nil
}
logError(StepWaitCallback, "授权超时")
return "", fmt.Errorf("授权超时")
}