Files

360 lines
10 KiB
Go
Raw Permalink 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 (
"context"
"fmt"
"strings"
"time"
"codex-pool/internal/proxyutil"
"github.com/chromedp/cdproto/cdp"
"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(&currentURL))
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(&currentURL))
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
}
}
// 等待页面跳转(等待 URL 变化或密码框出现,最多 10 秒)
passwordSelectors := []string{
`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 callbackURL != "" {
logStep(StepComplete, "授权成功")
return ExtractCodeFromCallbackURL(callbackURL), nil
}
_ = chromedp.Run(ctx, chromedp.Location(&currentURL))
if strings.Contains(currentURL, "code=") {
logStep(StepComplete, "授权成功")
return ExtractCodeFromCallbackURL(currentURL), nil
}
// 检查是否有密码输入框(页面已跳转)
for _, sel := range passwordSelectors {
var nodes []*cdp.Node
if err := chromedp.Run(ctx, chromedp.Nodes(sel, &nodes, chromedp.ByQuery)); err == nil && len(nodes) > 0 {
passwordFound = true
break
}
}
if passwordFound {
break
}
// 如果 URL 已经变化(包含 password也跳出
if strings.Contains(currentURL, "password") {
break
}
}
logStep(StepInputPassword, "查找密码框 | URL: %s", currentURL)
// 使用短超时查找密码框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(&currentURL))
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("授权超时")
}