feat: Implement S2A, Rod, and Chromedp based authentication for external services, and introduce new frontend pages and backend APIs for monitoring, configuration, upload, and team processes.
This commit is contained in:
@@ -11,13 +11,14 @@ import (
|
||||
|
||||
// MonitorSettings 监控设置
|
||||
type MonitorSettings struct {
|
||||
Target int `json:"target"`
|
||||
AutoAdd bool `json:"auto_add"`
|
||||
MinInterval int `json:"min_interval"`
|
||||
CheckInterval int `json:"check_interval"` // 自动补号检查间隔(秒)
|
||||
PollingEnabled bool `json:"polling_enabled"`
|
||||
PollingInterval int `json:"polling_interval"`
|
||||
ReplenishUseProxy bool `json:"replenish_use_proxy"` // 补号时使用代理
|
||||
Target int `json:"target"`
|
||||
AutoAdd bool `json:"auto_add"`
|
||||
MinInterval int `json:"min_interval"`
|
||||
CheckInterval int `json:"check_interval"` // 自动补号检查间隔(秒)
|
||||
PollingEnabled bool `json:"polling_enabled"`
|
||||
PollingInterval int `json:"polling_interval"`
|
||||
ReplenishUseProxy bool `json:"replenish_use_proxy"` // 补号时使用代理
|
||||
BrowserType string `json:"browser_type"` // 授权浏览器引擎: chromedp 或 rod
|
||||
}
|
||||
|
||||
// HandleGetMonitorSettings 获取监控设置
|
||||
@@ -40,6 +41,7 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
|
||||
PollingEnabled: false,
|
||||
PollingInterval: 60,
|
||||
ReplenishUseProxy: false,
|
||||
BrowserType: "chromedp", // 默认使用 chromedp
|
||||
}
|
||||
|
||||
if val, _ := database.Instance.GetConfig("monitor_target"); val != "" {
|
||||
@@ -71,6 +73,9 @@ func HandleGetMonitorSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if val, _ := database.Instance.GetConfig("monitor_replenish_use_proxy"); val == "true" {
|
||||
settings.ReplenishUseProxy = true
|
||||
}
|
||||
if val, _ := database.Instance.GetConfig("monitor_browser_type"); val != "" {
|
||||
settings.BrowserType = val
|
||||
}
|
||||
|
||||
Success(w, settings)
|
||||
}
|
||||
@@ -128,6 +133,14 @@ func HandleSaveMonitorSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if err := database.Instance.SetConfig("monitor_replenish_use_proxy", strconv.FormatBool(settings.ReplenishUseProxy)); err != nil {
|
||||
saveErrors = append(saveErrors, "replenish_use_proxy: "+err.Error())
|
||||
}
|
||||
// 浏览器类型默认为 chromedp
|
||||
browserType := settings.BrowserType
|
||||
if browserType != "chromedp" && browserType != "rod" {
|
||||
browserType = "chromedp"
|
||||
}
|
||||
if err := database.Instance.SetConfig("monitor_browser_type", browserType); err != nil {
|
||||
saveErrors = append(saveErrors, "browser_type: "+err.Error())
|
||||
}
|
||||
|
||||
if len(saveErrors) > 0 {
|
||||
errMsg := "保存监控设置部分失败: " + saveErrors[0]
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"codex-pool/internal/invite"
|
||||
"codex-pool/internal/logger"
|
||||
"codex-pool/internal/mail"
|
||||
"codex-pool/internal/proxyutil"
|
||||
"codex-pool/internal/register"
|
||||
)
|
||||
|
||||
@@ -127,6 +128,14 @@ func HandleTeamProcess(w http.ResponseWriter, r *http.Request) {
|
||||
if req.Proxy == "" && config.Global != nil {
|
||||
req.Proxy = config.Global.GetProxy() // 使用新的代理获取方法
|
||||
}
|
||||
if req.Proxy != "" {
|
||||
normalized, err := proxyutil.Normalize(req.Proxy)
|
||||
if err != nil {
|
||||
Error(w, http.StatusBadRequest, fmt.Sprintf("代理格式错误: %v", err))
|
||||
return
|
||||
}
|
||||
req.Proxy = normalized
|
||||
}
|
||||
|
||||
// 初始化状态
|
||||
teamProcessState.Running = true
|
||||
|
||||
@@ -6,12 +6,30 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codex-pool/internal/proxyutil"
|
||||
|
||||
"github.com/chromedp/cdproto/fetch"
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
// CompleteWithChromedp 使用 chromedp 完成 S2A OAuth 授权
|
||||
func CompleteWithChromedp(authURL, email, password, teamID string, headless bool, proxy string) (string, error) {
|
||||
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),
|
||||
@@ -21,8 +39,8 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
chromedp.UserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36"),
|
||||
)
|
||||
|
||||
if proxy != "" {
|
||||
opts = append(opts, chromedp.ProxyServer(proxy))
|
||||
if proxyServer != "" {
|
||||
opts = append(opts, chromedp.ProxyServer(proxyServer))
|
||||
}
|
||||
|
||||
allocCtx, cancel := chromedp.NewExecAllocator(context.Background(), opts...)
|
||||
@@ -37,8 +55,36 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
var callbackURL string
|
||||
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
if req, ok := ev.(*network.EventRequestWillBeSent); ok {
|
||||
url := req.Request.URL
|
||||
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)
|
||||
}()
|
||||
|
||||
case *network.EventRequestWillBeSent:
|
||||
url := ev.Request.URL
|
||||
if strings.Contains(url, "localhost") && strings.Contains(url, "code=") {
|
||||
callbackURL = url
|
||||
}
|
||||
@@ -46,6 +92,8 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
})
|
||||
|
||||
err := chromedp.Run(ctx,
|
||||
// Handle proxy auth (407) in headless mode.
|
||||
fetch.Enable().WithHandleAuthRequests(true),
|
||||
network.Enable(),
|
||||
chromedp.Navigate(authURL),
|
||||
chromedp.WaitReady("body"),
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"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"
|
||||
@@ -17,6 +19,8 @@ type RodAuth struct {
|
||||
browser *rod.Browser
|
||||
headless bool
|
||||
proxy string
|
||||
proxyUser string
|
||||
proxyPass string
|
||||
}
|
||||
|
||||
// getChromiumPath 获取 Chromium 路径
|
||||
@@ -49,6 +53,24 @@ func getChromiumPath() string {
|
||||
|
||||
// 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").
|
||||
@@ -67,8 +89,8 @@ func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
|
||||
l = l.Bin(chromiumPath)
|
||||
}
|
||||
|
||||
if proxy != "" {
|
||||
l = l.Proxy(proxy)
|
||||
if proxyServer != "" {
|
||||
l = l.Proxy(proxyServer)
|
||||
}
|
||||
|
||||
controlURL, err := l.Launch()
|
||||
@@ -85,6 +107,8 @@ func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
|
||||
browser: browser,
|
||||
headless: headless,
|
||||
proxy: proxy,
|
||||
proxyUser: proxyUser,
|
||||
proxyPass: proxyPass,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -97,6 +121,39 @@ func (r *RodAuth) 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)
|
||||
|
||||
@@ -10,6 +10,8 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"codex-pool/internal/proxyutil"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -214,8 +216,13 @@ func RefreshCodexToken(refreshToken string, proxyURL string) (*CodexTokens, erro
|
||||
client := &http.Client{Timeout: 30 * time.Second}
|
||||
|
||||
if proxyURL != "" {
|
||||
proxyURLParsed, _ := url.Parse(proxyURL)
|
||||
client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyURLParsed)}
|
||||
info, err := proxyutil.Parse(proxyURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("代理格式错误: %v", err)
|
||||
}
|
||||
if info.URL != nil {
|
||||
client.Transport = &http.Transport{Proxy: http.ProxyURL(info.URL)}
|
||||
}
|
||||
}
|
||||
|
||||
data := url.Values{
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"codex-pool/internal/proxyutil"
|
||||
|
||||
"github.com/andybalholm/brotli"
|
||||
http2 "github.com/bogdanfinn/fhttp"
|
||||
tls_client "github.com/bogdanfinn/tls-client"
|
||||
@@ -44,7 +46,11 @@ func New(proxyStr string) (*TLSClient, error) {
|
||||
}
|
||||
|
||||
if proxyStr != "" {
|
||||
options = append(options, tls_client.WithProxyUrl(proxyStr))
|
||||
normalized, err := proxyutil.Normalize(proxyStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
options = append(options, tls_client.WithProxyUrl(normalized))
|
||||
}
|
||||
|
||||
client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...)
|
||||
|
||||
107
backend/internal/proxyutil/proxy.go
Normal file
107
backend/internal/proxyutil/proxy.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package proxyutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Info is a parsed proxy configuration.
|
||||
//
|
||||
// It supports the following input formats:
|
||||
// - http://host:port
|
||||
// - http://user:pass@host:port
|
||||
// - host:port (defaults to http)
|
||||
// - host:port:user:pass (defaults to http; password may contain ':')
|
||||
// - user:pass@host:port (defaults to http)
|
||||
type Info struct {
|
||||
// URL is the normalized proxy URL (may include credentials).
|
||||
URL *url.URL
|
||||
// Server is the proxy server URL without credentials (scheme + host:port).
|
||||
Server *url.URL
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
func Parse(raw string) (*Info, error) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return &Info{}, nil
|
||||
}
|
||||
|
||||
// URL-style proxy (has scheme)
|
||||
if strings.Contains(raw, "://") {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse proxy url: %w", err)
|
||||
}
|
||||
if u.Scheme == "" || u.Host == "" {
|
||||
return nil, fmt.Errorf("invalid proxy url: %q", raw)
|
||||
}
|
||||
|
||||
u = &url.URL{
|
||||
Scheme: strings.ToLower(u.Scheme),
|
||||
Host: u.Host,
|
||||
User: u.User,
|
||||
}
|
||||
|
||||
user := ""
|
||||
pass := ""
|
||||
if u.User != nil {
|
||||
user = u.User.Username()
|
||||
pass, _ = u.User.Password()
|
||||
}
|
||||
|
||||
server := &url.URL{Scheme: u.Scheme, Host: u.Host}
|
||||
return &Info{
|
||||
URL: u,
|
||||
Server: server,
|
||||
Username: user,
|
||||
Password: pass,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// user:pass@host:port (no scheme)
|
||||
if strings.Contains(raw, "@") {
|
||||
return Parse("http://" + raw)
|
||||
}
|
||||
|
||||
// host:port[:user:pass]
|
||||
parts := strings.Split(raw, ":")
|
||||
switch {
|
||||
case len(parts) == 2:
|
||||
host, port := parts[0], parts[1]
|
||||
hp := net.JoinHostPort(host, port)
|
||||
u := &url.URL{Scheme: "http", Host: hp}
|
||||
return &Info{URL: u, Server: &url.URL{Scheme: u.Scheme, Host: u.Host}}, nil
|
||||
case len(parts) >= 4:
|
||||
host, port := parts[0], parts[1]
|
||||
user := parts[2]
|
||||
pass := strings.Join(parts[3:], ":")
|
||||
hp := net.JoinHostPort(host, port)
|
||||
u := &url.URL{Scheme: "http", Host: hp, User: url.UserPassword(user, pass)}
|
||||
return &Info{
|
||||
URL: u,
|
||||
Server: &url.URL{Scheme: u.Scheme, Host: u.Host},
|
||||
Username: user,
|
||||
Password: pass,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported proxy format: %q", raw)
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize returns a normalized proxy URL string.
|
||||
// Empty input returns empty output.
|
||||
func Normalize(raw string) (string, error) {
|
||||
info, err := Parse(raw)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if info.URL == nil {
|
||||
return "", nil
|
||||
}
|
||||
return info.URL.String(), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user