339 lines
9.5 KiB
Go
339 lines
9.5 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...)
|
||
}
|
||
}
|
||
|
||
// 获取随机浏览器配置
|
||
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()
|
||
|
||
// 设置合理的超时时间 30 秒
|
||
ctx, cancel = context.WithTimeout(ctx, 30*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...)
|
||
}
|
||
|
||
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(StepComplete, "授权成功(快速通道)")
|
||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||
}
|
||
|
||
var currentURL string
|
||
_ = chromedp.Run(ctx, chromedp.Location(¤tURL))
|
||
logStep(StepNavigate, "页面加载完成 | URL: %s", currentURL)
|
||
|
||
if strings.Contains(currentURL, "code=") {
|
||
logStep(StepComplete, "授权成功(快速通道)")
|
||
return ExtractCodeFromCallbackURL(currentURL), nil
|
||
}
|
||
|
||
time.Sleep(1 * time.Second)
|
||
|
||
// 邮箱输入框选择器
|
||
emailSelectors := []string{
|
||
`input[name="email"]`,
|
||
`input[type="email"]`,
|
||
`input[name="username"]`,
|
||
`input[id="email"]`,
|
||
`input[autocomplete="email"]`,
|
||
}
|
||
|
||
// 创建带短超时的上下文用于查找元素(10秒)
|
||
findCtx, findCancel := context.WithTimeout(ctx, 10*time.Second)
|
||
|
||
var emailFilled bool
|
||
for _, sel := range emailSelectors {
|
||
err = chromedp.Run(findCtx, 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, "邮箱已填写 | 选择器: %s", sel)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
findCancel()
|
||
|
||
if !emailFilled {
|
||
_ = chromedp.Run(ctx, chromedp.Location(¤tURL))
|
||
logError(StepInputEmail, "未找到邮箱输入框 | URL: %s", currentURL)
|
||
return "", fmt.Errorf("未找到邮箱输入框")
|
||
}
|
||
|
||
time.Sleep(300 * time.Millisecond)
|
||
|
||
buttonSelectors := []string{
|
||
`button[type="submit"]`,
|
||
`div._ctas_1alro_13 button`,
|
||
`button[data-testid="login-button"]`,
|
||
`button.continue-btn`,
|
||
`input[type="submit"]`,
|
||
`button[name="action"]`,
|
||
}
|
||
|
||
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(StepComplete, "授权成功")
|
||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||
}
|
||
|
||
_ = chromedp.Run(ctx, chromedp.Location(¤tURL))
|
||
if strings.Contains(currentURL, "code=") {
|
||
logStep(StepComplete, "授权成功")
|
||
return ExtractCodeFromCallbackURL(currentURL), nil
|
||
}
|
||
|
||
logStep(StepInputPassword, "查找密码框 | URL: %s", currentURL)
|
||
|
||
// 密码输入框选择器
|
||
passwordSelectors := []string{
|
||
`input[name="current-password"]`,
|
||
`input[autocomplete="current-password"]`,
|
||
`input[type="password"]`,
|
||
`input[name="password"]`,
|
||
`input[id="password"]`,
|
||
}
|
||
|
||
// 使用短超时查找密码框(10秒)
|
||
findCtx2, findCancel2 := context.WithTimeout(ctx, 10*time.Second)
|
||
|
||
var passwordFilled bool
|
||
for _, sel := range passwordSelectors {
|
||
err = chromedp.Run(findCtx2, 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, "密码已填写 | 选择器: %s", sel)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
findCancel2()
|
||
|
||
if !passwordFilled {
|
||
_ = chromedp.Run(ctx, chromedp.Location(¤tURL))
|
||
logError(StepInputPassword, "未找到密码输入框 | URL: %s", currentURL)
|
||
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
|
||
}
|
||
}
|
||
|
||
// 等待授权回调(最多15秒)
|
||
for i := 0; i < 30; i++ {
|
||
time.Sleep(500 * time.Millisecond)
|
||
|
||
if callbackURL != "" {
|
||
logStep(StepComplete, "授权成功")
|
||
return ExtractCodeFromCallbackURL(callbackURL), nil
|
||
}
|
||
|
||
var url string
|
||
if err := chromedp.Run(ctx, chromedp.Location(&url)); err == nil {
|
||
if strings.Contains(url, "code=") {
|
||
logStep(StepComplete, "授权成功")
|
||
return ExtractCodeFromCallbackURL(url), nil
|
||
}
|
||
|
||
if strings.Contains(url, "consent") {
|
||
logStep(StepConsent, "处理授权同意... | URL: %s", url)
|
||
// 同意页面的确认按钮(第二个按钮)
|
||
consentSelectors := []string{
|
||
`div._ctas_1alro_13 div:nth-child(2) button`,
|
||
`button[type="submit"]`,
|
||
`div._ctas_1alro_13 button`,
|
||
}
|
||
for _, sel := range consentSelectors {
|
||
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, "选择工作区...")
|
||
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("授权超时")
|
||
}
|