feat: Add a new cleaner page for managing and automatically cleaning error S2A accounts, supported by new backend services for logging, authentication, and client operations.
This commit is contained in:
@@ -332,7 +332,7 @@ func handleClearLogsByModule(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// handleQueryLogs GET /api/logs/query?module=cleaner&page=1&page_size=5
|
||||
// handleQueryLogs GET /api/logs/query?module=cleaner&page=1&page_size=5&level=success
|
||||
func handleQueryLogs(w http.ResponseWriter, r *http.Request) {
|
||||
module := r.URL.Query().Get("module")
|
||||
if module == "" {
|
||||
@@ -342,6 +342,8 @@ func handleQueryLogs(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
page := 1
|
||||
pageSize := 5
|
||||
level := r.URL.Query().Get("level") // 可选的日志级别过滤
|
||||
|
||||
if v := r.URL.Query().Get("page"); v != "" {
|
||||
if p, err := strconv.Atoi(v); err == nil && p > 0 {
|
||||
page = p
|
||||
@@ -353,7 +355,14 @@ func handleQueryLogs(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
entries, total := logger.GetLogsByModule(module, page, pageSize)
|
||||
// 如果指定了 level,使用带级别过滤的函数
|
||||
var entries []logger.LogEntry
|
||||
var total int
|
||||
if level != "" {
|
||||
entries, total = logger.GetLogsByModuleAndLevel(module, level, page, pageSize)
|
||||
} else {
|
||||
entries, total = logger.GetLogsByModule(module, page, pageSize)
|
||||
}
|
||||
totalPages := (total + pageSize - 1) / pageSize
|
||||
|
||||
api.Success(w, map[string]interface{}{
|
||||
|
||||
@@ -1,433 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"codex-pool/internal/auth"
|
||||
"codex-pool/internal/config"
|
||||
"codex-pool/internal/invite"
|
||||
"codex-pool/internal/mail"
|
||||
"codex-pool/internal/register"
|
||||
)
|
||||
|
||||
type Account struct {
|
||||
Account string `json:"account"`
|
||||
Password string `json:"password"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type MemberAccount struct {
|
||||
Email string
|
||||
Password string
|
||||
Success bool
|
||||
}
|
||||
|
||||
const (
|
||||
MembersPerTeam = 4 // 每个 team 注册的成员数
|
||||
NumTeams = 2 // 并发运行的 team 数量
|
||||
)
|
||||
|
||||
// ANSI 颜色码
|
||||
const (
|
||||
ColorReset = "\033[0m"
|
||||
ColorRed = "\033[31m"
|
||||
ColorGreen = "\033[32m"
|
||||
ColorYellow = "\033[33m"
|
||||
ColorBlue = "\033[34m"
|
||||
ColorMagenta = "\033[35m"
|
||||
ColorCyan = "\033[36m"
|
||||
ColorWhite = "\033[37m"
|
||||
ColorBold = "\033[1m"
|
||||
)
|
||||
|
||||
// Team 颜色
|
||||
var teamColors = []string{
|
||||
ColorCyan, // Team 1
|
||||
ColorMagenta, // Team 2
|
||||
ColorYellow, // Team 3
|
||||
ColorBlue, // Team 4
|
||||
}
|
||||
|
||||
// TeamLogger 带颜色的Team日志
|
||||
type TeamLogger struct {
|
||||
prefix string
|
||||
color string
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func NewTeamLogger(teamIdx int) *TeamLogger {
|
||||
color := teamColors[teamIdx%len(teamColors)]
|
||||
return &TeamLogger{
|
||||
prefix: fmt.Sprintf("[Team %d]", teamIdx+1),
|
||||
color: color,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *TeamLogger) Log(format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s%s%s %s\n", l.color, l.prefix, ColorReset, msg)
|
||||
}
|
||||
|
||||
func (l *TeamLogger) Success(format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s%s%s %s✓%s %s\n", l.color, l.prefix, ColorReset, ColorGreen, ColorReset, msg)
|
||||
}
|
||||
|
||||
func (l *TeamLogger) Error(format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s%s%s %s✗%s %s\n", l.color, l.prefix, ColorReset, ColorRed, ColorReset, msg)
|
||||
}
|
||||
|
||||
func (l *TeamLogger) Info(format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s%s%s %s→%s %s\n", l.color, l.prefix, ColorReset, ColorYellow, ColorReset, msg)
|
||||
}
|
||||
|
||||
// Highlight 整行绿色高亮(用于重要成功信息)
|
||||
func (l *TeamLogger) Highlight(format string, args ...interface{}) {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
msg := fmt.Sprintf(format, args...)
|
||||
fmt.Printf("%s%s %s✓ %s%s\n", ColorGreen, l.prefix, ColorBold, msg, ColorReset)
|
||||
}
|
||||
|
||||
func main() {
|
||||
fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset)
|
||||
fmt.Printf("%s Multi-Team Concurrent Test (Chromedp)%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf(" - %d Teams running concurrently\n", NumTeams)
|
||||
fmt.Printf(" - %d Members per team\n", MembersPerTeam)
|
||||
fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset)
|
||||
fmt.Println()
|
||||
|
||||
// 加载配置
|
||||
configPath := config.FindPath()
|
||||
cfg, err := config.Load(configPath)
|
||||
if err != nil {
|
||||
fmt.Printf("%s[Error]%s Failed to load config: %v\n", ColorRed, ColorReset, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// 初始化邮箱服务
|
||||
if len(cfg.MailServices) > 0 {
|
||||
mail.Init(cfg.MailServices)
|
||||
}
|
||||
|
||||
// 加载账号
|
||||
accountsFile := "accounts-3-20260130-052841.json"
|
||||
data, err := os.ReadFile(accountsFile)
|
||||
if err != nil {
|
||||
fmt.Printf("%s[Error]%s Failed to read accounts file: %v\n", ColorRed, ColorReset, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
var accounts []Account
|
||||
if err := json.Unmarshal(data, &accounts); err != nil {
|
||||
fmt.Printf("%s[Error]%s Failed to parse accounts file: %v\n", ColorRed, ColorReset, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(accounts) < NumTeams {
|
||||
fmt.Printf("%s[Error]%s Need at least %d owner accounts, got %d\n", ColorRed, ColorReset, NumTeams, len(accounts))
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
proxy := cfg.DefaultProxy
|
||||
if proxy == "" {
|
||||
proxy = "http://127.0.0.1:7890"
|
||||
}
|
||||
fmt.Printf("[Proxy] %s\n", proxy)
|
||||
fmt.Println()
|
||||
|
||||
// 显示 Owner 列表
|
||||
fmt.Printf("%s========================================%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf("%s[Owners]%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf("%s========================================%s\n", ColorBold, ColorReset)
|
||||
for i := 0; i < NumTeams; i++ {
|
||||
color := teamColors[i%len(teamColors)]
|
||||
fmt.Printf(" %sTeam %d:%s %s\n", color, i+1, ColorReset, accounts[i].Account)
|
||||
}
|
||||
fmt.Println()
|
||||
|
||||
// 并发运行多个 Team
|
||||
var wg sync.WaitGroup
|
||||
var totalRegistered int32
|
||||
var totalS2A int32
|
||||
startTime := time.Now()
|
||||
|
||||
for teamIdx := 0; teamIdx < NumTeams; teamIdx++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
registered, s2a := runTeam(idx, accounts[idx], cfg, proxy)
|
||||
atomic.AddInt32(&totalRegistered, int32(registered))
|
||||
atomic.AddInt32(&totalS2A, int32(s2a))
|
||||
}(teamIdx)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
totalDuration := time.Since(startTime)
|
||||
|
||||
// 总结
|
||||
fmt.Println()
|
||||
fmt.Printf("%s%s=================================================================%s\n", ColorBold, ColorWhite, ColorReset)
|
||||
fmt.Printf("%s All Teams Complete%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf("%s=================================================================%s\n", ColorBold, ColorReset)
|
||||
fmt.Printf(" Total Registered: %s%d/%d%s\n", ColorGreen, totalRegistered, NumTeams*MembersPerTeam, ColorReset)
|
||||
fmt.Printf(" Total Added to S2A: %s%d%s\n", ColorGreen, totalS2A, ColorReset)
|
||||
fmt.Printf(" Total Duration: %v\n", totalDuration)
|
||||
fmt.Printf("%s=================================================================%s\n", ColorBold, ColorReset)
|
||||
}
|
||||
|
||||
// runTeam 运行单个 Team 的流程
|
||||
func runTeam(teamIdx int, owner Account, cfg *config.Config, proxy string) (registered, s2a int) {
|
||||
log := NewTeamLogger(teamIdx)
|
||||
|
||||
log.Log("Starting with owner: %s", owner.Account)
|
||||
|
||||
// Step 1: 获取 Team ID
|
||||
log.Info("Fetching Team ID...")
|
||||
inviter := invite.NewWithProxy(owner.Token, proxy)
|
||||
teamID, err := inviter.GetAccountID()
|
||||
if err != nil {
|
||||
log.Error("Failed to get Team ID: %v", err)
|
||||
return 0, 0
|
||||
}
|
||||
log.Success("Team ID: %s", teamID)
|
||||
|
||||
// Step 2: 生成成员邮箱
|
||||
log.Info("Generating %d member emails...", MembersPerTeam)
|
||||
children := make([]MemberAccount, MembersPerTeam)
|
||||
for i := 0; i < MembersPerTeam; i++ {
|
||||
children[i].Email = mail.GenerateEmail()
|
||||
children[i].Password = register.GeneratePassword()
|
||||
log.Log("[Member %d] Email: %s", i+1, children[i].Email)
|
||||
}
|
||||
|
||||
// 批量发送邀请
|
||||
log.Info("Sending invites...")
|
||||
inviteEmails := make([]string, MembersPerTeam)
|
||||
for i, c := range children {
|
||||
inviteEmails[i] = c.Email
|
||||
}
|
||||
if err := inviter.SendInvites(inviteEmails); err != nil {
|
||||
log.Error("Failed to send invites: %v", err)
|
||||
return 0, 0
|
||||
}
|
||||
log.Success("Sent %d invite(s)", len(inviteEmails))
|
||||
|
||||
// Step 3: 并发注册成员
|
||||
log.Info("Starting member registration...")
|
||||
var memberWg sync.WaitGroup
|
||||
var successCount int32
|
||||
memberMutex := sync.Mutex{}
|
||||
|
||||
for i := range children {
|
||||
memberWg.Add(1)
|
||||
go func(memberIdx int) {
|
||||
defer memberWg.Done()
|
||||
|
||||
memberMutex.Lock()
|
||||
email := children[memberIdx].Email
|
||||
password := children[memberIdx].Password
|
||||
memberMutex.Unlock()
|
||||
|
||||
name := register.GenerateName()
|
||||
birthdate := register.GenerateBirthdate()
|
||||
|
||||
// 最多重试3次
|
||||
for attempt := 0; attempt < 3; attempt++ {
|
||||
if attempt > 0 {
|
||||
email = mail.GenerateEmail()
|
||||
password = register.GeneratePassword()
|
||||
log.Log("[Member %d] Retry %d - New email: %s", memberIdx+1, attempt, email)
|
||||
|
||||
// 发送新邀请
|
||||
if err := inviter.SendInvites([]string{email}); err != nil {
|
||||
log.Error("[Member %d] Failed to send retry invite: %v", memberIdx+1, err)
|
||||
continue
|
||||
}
|
||||
log.Success("[Member %d] Sent retry invite", memberIdx+1)
|
||||
}
|
||||
|
||||
// 详细注册流程
|
||||
if err := registerMemberDetailed(log, memberIdx+1, email, password, name, birthdate, proxy); err != nil {
|
||||
if strings.Contains(err.Error(), "验证码") {
|
||||
log.Error("[Member %d] OTP timeout, will retry...", memberIdx+1)
|
||||
continue
|
||||
}
|
||||
log.Error("[Member %d] Registration failed: %v", memberIdx+1, err)
|
||||
return
|
||||
}
|
||||
|
||||
// 成功
|
||||
memberMutex.Lock()
|
||||
children[memberIdx].Email = email
|
||||
children[memberIdx].Password = password
|
||||
children[memberIdx].Success = true
|
||||
memberMutex.Unlock()
|
||||
|
||||
atomic.AddInt32(&successCount, 1)
|
||||
log.Success("[Member %d] Registration complete!", memberIdx+1)
|
||||
return
|
||||
}
|
||||
|
||||
log.Error("[Member %d] Failed after 3 retries", memberIdx+1)
|
||||
}(i)
|
||||
}
|
||||
|
||||
memberWg.Wait()
|
||||
registered = int(successCount)
|
||||
log.Success("Registration phase complete: %d/%d", registered, MembersPerTeam)
|
||||
|
||||
// 收集成功的成员
|
||||
var registeredChildren []MemberAccount
|
||||
for _, c := range children {
|
||||
if c.Success {
|
||||
registeredChildren = append(registeredChildren, c)
|
||||
}
|
||||
}
|
||||
|
||||
if len(registeredChildren) == 0 {
|
||||
log.Error("No members registered")
|
||||
return registered, 0
|
||||
}
|
||||
|
||||
// Step 4: 串行入库
|
||||
log.Info("Starting S2A authorization...")
|
||||
|
||||
for i, child := range registeredChildren {
|
||||
log.Log("[Member %d] Getting S2A auth URL...", i+1)
|
||||
s2aResp, err := auth.GenerateS2AAuthURL(cfg.S2AApiBase, cfg.S2AAdminKey, cfg.ProxyID)
|
||||
if err != nil {
|
||||
log.Error("[Member %d] Auth URL failed: %v", i+1, err)
|
||||
continue
|
||||
}
|
||||
log.Success("[Member %d] Got auth URL", i+1)
|
||||
|
||||
log.Log("[Member %d] Running browser automation (Chromedp)...", i+1)
|
||||
code, err := auth.CompleteWithChromedp(s2aResp.Data.AuthURL, child.Email, child.Password, teamID, true, proxy)
|
||||
if err != nil {
|
||||
log.Error("[Member %d] Browser auth failed: %v", i+1, err)
|
||||
continue
|
||||
}
|
||||
log.Success("[Member %d] Browser auth complete", i+1)
|
||||
|
||||
log.Log("[Member %d] Submitting to S2A...", i+1)
|
||||
result, err := auth.SubmitS2AOAuth(
|
||||
cfg.S2AApiBase,
|
||||
cfg.S2AAdminKey,
|
||||
s2aResp.Data.SessionID,
|
||||
code,
|
||||
child.Email,
|
||||
cfg.Concurrency,
|
||||
cfg.Priority,
|
||||
cfg.GroupIDs,
|
||||
cfg.ProxyID,
|
||||
)
|
||||
if err != nil {
|
||||
log.Error("[Member %d] S2A submit failed: %v", i+1, err)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Highlight("[Member %d] Added to S2A! ID=%d, Status=%s", i+1, result.Data.ID, result.Data.Status)
|
||||
s2a++
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
log.Success("Team complete: %d registered, %d in S2A", registered, s2a)
|
||||
return registered, s2a
|
||||
}
|
||||
|
||||
// registerMemberDetailed 详细的注册流程,带日志
|
||||
func registerMemberDetailed(log *TeamLogger, memberNum int, email, password, name, birthdate, proxy string) error {
|
||||
prefix := fmt.Sprintf("[Member %d]", memberNum)
|
||||
|
||||
log.Log("%s Creating TLS client...", prefix)
|
||||
reg, err := register.New(proxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Log("%s Initializing session...", prefix)
|
||||
if err := reg.InitSession(); err != nil {
|
||||
return fmt.Errorf("初始化失败: %v", err)
|
||||
}
|
||||
log.Success("%s Session initialized", prefix)
|
||||
|
||||
log.Log("%s Getting authorize URL...", prefix)
|
||||
if err := reg.GetAuthorizeURL(email); err != nil {
|
||||
return fmt.Errorf("获取授权URL失败: %v", err)
|
||||
}
|
||||
log.Success("%s Got authorize URL", prefix)
|
||||
|
||||
log.Log("%s Starting authorize flow...", prefix)
|
||||
if err := reg.StartAuthorize(); err != nil {
|
||||
return fmt.Errorf("启动授权失败: %v", err)
|
||||
}
|
||||
log.Success("%s Authorize flow started", prefix)
|
||||
|
||||
log.Log("%s Registering account...", prefix)
|
||||
if err := reg.Register(email, password); err != nil {
|
||||
return fmt.Errorf("注册失败: %v", err)
|
||||
}
|
||||
log.Success("%s Account registered", prefix)
|
||||
|
||||
log.Log("%s Sending verification email...", prefix)
|
||||
if err := reg.SendVerificationEmail(); err != nil {
|
||||
return fmt.Errorf("发送邮件失败: %v", err)
|
||||
}
|
||||
log.Success("%s Verification email sent", prefix)
|
||||
|
||||
log.Log("%s Waiting for OTP code (5s timeout)...", prefix)
|
||||
otpCode, err := mail.GetVerificationCode(email, 5*time.Second)
|
||||
if err != nil {
|
||||
log.Log("%s OTP not received in 5s, waiting 15s more...", prefix)
|
||||
otpCode, err = mail.GetVerificationCode(email, 15*time.Second)
|
||||
if err != nil {
|
||||
return fmt.Errorf("验证码获取超时")
|
||||
}
|
||||
}
|
||||
log.Success("%s Got OTP: %s", prefix, otpCode)
|
||||
|
||||
log.Log("%s Validating OTP...", prefix)
|
||||
if err := reg.ValidateOTP(otpCode); err != nil {
|
||||
return fmt.Errorf("OTP验证失败: %v", err)
|
||||
}
|
||||
log.Success("%s OTP validated", prefix)
|
||||
|
||||
log.Log("%s Creating account (name=%s, birthdate=%s)...", prefix, name, birthdate)
|
||||
if err := reg.CreateAccount(name, birthdate); err != nil {
|
||||
return fmt.Errorf("创建账户失败: %v", err)
|
||||
}
|
||||
log.Success("%s Account created", prefix)
|
||||
|
||||
log.Log("%s Getting session token...", prefix)
|
||||
_ = reg.GetSessionToken()
|
||||
if reg.AccessToken != "" {
|
||||
log.Success("%s Got access token: %s...", prefix, truncate(reg.AccessToken, 30))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func truncate(s string, maxLen int) string {
|
||||
if len(s) <= maxLen {
|
||||
return s
|
||||
}
|
||||
return s[:maxLen]
|
||||
}
|
||||
261
backend/internal/auth/browser_profiles.go
Normal file
261
backend/internal/auth/browser_profiles.go
Normal file
@@ -0,0 +1,261 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 浏览器自动化指纹配置
|
||||
// 用于 chromedp/rod 的随机指纹生成
|
||||
// ============================================================
|
||||
|
||||
// BrowserProfile 浏览器配置档案
|
||||
type BrowserProfile struct {
|
||||
UserAgent string
|
||||
Platform string
|
||||
Width int
|
||||
Height int
|
||||
AcceptLang string
|
||||
ChromeVer string
|
||||
SecChUa string
|
||||
WebGLVendor string
|
||||
WebGLRender string
|
||||
PixelRatio float64
|
||||
MaxTouch int
|
||||
HardwareConc int // navigator.hardwareConcurrency
|
||||
}
|
||||
|
||||
// Windows 配置池
|
||||
var windowsProfiles = []BrowserProfile{
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
||||
Platform: "Win32",
|
||||
Width: 1920, Height: 1080,
|
||||
AcceptLang: "en-US,en;q=0.9",
|
||||
ChromeVer: "133",
|
||||
SecChUa: `"Chromium";v="133", "Not(A:Brand";v="99", "Google Chrome";v="133"`,
|
||||
WebGLVendor: "Google Inc. (NVIDIA)",
|
||||
WebGLRender: "ANGLE (NVIDIA, NVIDIA GeForce RTX 3070 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
PixelRatio: 1,
|
||||
MaxTouch: 0,
|
||||
HardwareConc: 12,
|
||||
},
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
|
||||
Platform: "Win32",
|
||||
Width: 2560, Height: 1440,
|
||||
AcceptLang: "en-GB,en;q=0.9",
|
||||
ChromeVer: "132",
|
||||
SecChUa: `"Chromium";v="132", "Not A(Brand";v="99", "Google Chrome";v="132"`,
|
||||
WebGLVendor: "Google Inc. (AMD)",
|
||||
WebGLRender: "ANGLE (AMD, AMD Radeon RX 6800 XT Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
PixelRatio: 1,
|
||||
MaxTouch: 0,
|
||||
HardwareConc: 16,
|
||||
},
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 11.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
||||
Platform: "Win32",
|
||||
Width: 1366, Height: 768,
|
||||
AcceptLang: "en-US,en;q=0.9,zh-CN;q=0.8",
|
||||
ChromeVer: "133",
|
||||
SecChUa: `"Chromium";v="133", "Not/A)Brand";v="99", "Google Chrome";v="133"`,
|
||||
WebGLVendor: "Google Inc. (Intel)",
|
||||
WebGLRender: "ANGLE (Intel, Intel(R) UHD Graphics 630 Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
PixelRatio: 1.25,
|
||||
MaxTouch: 0,
|
||||
HardwareConc: 8,
|
||||
},
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
Platform: "Win32",
|
||||
Width: 1680, Height: 1050,
|
||||
AcceptLang: "de-DE,de;q=0.9,en;q=0.8",
|
||||
ChromeVer: "131",
|
||||
SecChUa: `"Chromium";v="131", "Not(A:Brand";v="99", "Google Chrome";v="131"`,
|
||||
WebGLVendor: "Google Inc. (NVIDIA)",
|
||||
WebGLRender: "ANGLE (NVIDIA, NVIDIA GeForce GTX 1660 Super Direct3D11 vs_5_0 ps_5_0, D3D11)",
|
||||
PixelRatio: 1,
|
||||
MaxTouch: 0,
|
||||
HardwareConc: 6,
|
||||
},
|
||||
}
|
||||
|
||||
// macOS 配置池
|
||||
var macProfiles = []BrowserProfile{
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
||||
Platform: "MacIntel",
|
||||
Width: 1440, Height: 900,
|
||||
AcceptLang: "en-US,en;q=0.9",
|
||||
ChromeVer: "133",
|
||||
SecChUa: `"Chromium";v="133", "Not(A:Brand";v="99", "Google Chrome";v="133"`,
|
||||
WebGLVendor: "Google Inc. (Apple)",
|
||||
WebGLRender: "ANGLE (Apple, Apple M1 Pro, OpenGL 4.1)",
|
||||
PixelRatio: 2,
|
||||
MaxTouch: 0,
|
||||
HardwareConc: 10,
|
||||
},
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.6834.110 Safari/537.36",
|
||||
Platform: "MacIntel",
|
||||
Width: 1920, Height: 1200,
|
||||
AcceptLang: "en-GB,en;q=0.9,en-US;q=0.8",
|
||||
ChromeVer: "132",
|
||||
SecChUa: `"Chromium";v="132", "Not A(Brand";v="99", "Google Chrome";v="132"`,
|
||||
WebGLVendor: "Google Inc. (Apple)",
|
||||
WebGLRender: "ANGLE (Apple, Apple M2 Max, OpenGL 4.1)",
|
||||
PixelRatio: 2,
|
||||
MaxTouch: 0,
|
||||
HardwareConc: 12,
|
||||
},
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 13_0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
|
||||
Platform: "MacIntel",
|
||||
Width: 2560, Height: 1600,
|
||||
AcceptLang: "fr-FR,fr;q=0.9,en;q=0.8",
|
||||
ChromeVer: "131",
|
||||
SecChUa: `"Chromium";v="131", "Not/A)Brand";v="99", "Google Chrome";v="131"`,
|
||||
WebGLVendor: "Google Inc. (Apple)",
|
||||
WebGLRender: "ANGLE (Apple, Apple M3, OpenGL 4.1)",
|
||||
PixelRatio: 2,
|
||||
MaxTouch: 0,
|
||||
HardwareConc: 8,
|
||||
},
|
||||
}
|
||||
|
||||
// Linux 配置池
|
||||
var linuxProfiles = []BrowserProfile{
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
|
||||
Platform: "Linux x86_64",
|
||||
Width: 1920, Height: 1080,
|
||||
AcceptLang: "en-US,en;q=0.9",
|
||||
ChromeVer: "133",
|
||||
SecChUa: `"Chromium";v="133", "Not(A:Brand";v="99", "Google Chrome";v="133"`,
|
||||
WebGLVendor: "Google Inc. (AMD)",
|
||||
WebGLRender: "ANGLE (AMD, AMD Radeon Graphics (renoir), OpenGL 4.6)",
|
||||
PixelRatio: 1,
|
||||
MaxTouch: 0,
|
||||
HardwareConc: 8,
|
||||
},
|
||||
{
|
||||
UserAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36",
|
||||
Platform: "Linux x86_64",
|
||||
Width: 1920, Height: 1200,
|
||||
AcceptLang: "en-GB,en;q=0.9",
|
||||
ChromeVer: "132",
|
||||
SecChUa: `"Chromium";v="132", "Not A(Brand";v="99", "Google Chrome";v="132"`,
|
||||
WebGLVendor: "Google Inc. (NVIDIA)",
|
||||
WebGLRender: "ANGLE (NVIDIA, NVIDIA GeForce RTX 2080, OpenGL 4.6)",
|
||||
PixelRatio: 1,
|
||||
MaxTouch: 0,
|
||||
HardwareConc: 12,
|
||||
},
|
||||
}
|
||||
|
||||
// 合并所有配置
|
||||
var allBrowserProfiles []BrowserProfile
|
||||
|
||||
func init() {
|
||||
allBrowserProfiles = make([]BrowserProfile, 0, 20)
|
||||
// Windows 权重更高(更常见)
|
||||
allBrowserProfiles = append(allBrowserProfiles, windowsProfiles...)
|
||||
allBrowserProfiles = append(allBrowserProfiles, windowsProfiles...)
|
||||
allBrowserProfiles = append(allBrowserProfiles, macProfiles...)
|
||||
allBrowserProfiles = append(allBrowserProfiles, linuxProfiles...)
|
||||
}
|
||||
|
||||
// GetRandomBrowserProfile 获取随机浏览器配置
|
||||
func GetRandomBrowserProfile() BrowserProfile {
|
||||
return allBrowserProfiles[rand.Intn(len(allBrowserProfiles))]
|
||||
}
|
||||
|
||||
// GetAntiDetectionJS 获取反检测 JavaScript 代码
|
||||
func GetAntiDetectionJS(profile BrowserProfile) string {
|
||||
return fmt.Sprintf(`
|
||||
// 隐藏 webdriver
|
||||
Object.defineProperty(navigator, 'webdriver', {
|
||||
get: () => undefined,
|
||||
});
|
||||
|
||||
// 删除 CDP 检测
|
||||
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Array;
|
||||
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Promise;
|
||||
delete window.cdc_adoQpoasnfa76pfcZLmcfl_Symbol;
|
||||
|
||||
// Chrome 对象
|
||||
window.chrome = {
|
||||
runtime: {},
|
||||
loadTimes: function() { return {}; },
|
||||
csi: function() { return {}; },
|
||||
app: {},
|
||||
};
|
||||
|
||||
// 隐藏自动化特征
|
||||
Object.defineProperty(navigator, 'plugins', {
|
||||
get: () => [
|
||||
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
|
||||
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
|
||||
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '' },
|
||||
],
|
||||
});
|
||||
|
||||
Object.defineProperty(navigator, 'languages', {
|
||||
get: () => ['%s'.split(',')[0], 'en'],
|
||||
});
|
||||
|
||||
Object.defineProperty(navigator, 'platform', {
|
||||
get: () => '%s',
|
||||
});
|
||||
|
||||
Object.defineProperty(navigator, 'hardwareConcurrency', {
|
||||
get: () => %d,
|
||||
});
|
||||
|
||||
Object.defineProperty(navigator, 'deviceMemory', {
|
||||
get: () => 8,
|
||||
});
|
||||
|
||||
Object.defineProperty(screen, 'colorDepth', {
|
||||
get: () => 24,
|
||||
});
|
||||
|
||||
// 伪装 WebGL
|
||||
const getParameterProxyHandler = {
|
||||
apply: function(target, thisArg, args) {
|
||||
const param = args[0];
|
||||
const gl = thisArg;
|
||||
// UNMASKED_VENDOR_WEBGL
|
||||
if (param === 37445) {
|
||||
return '%s';
|
||||
}
|
||||
// UNMASKED_RENDERER_WEBGL
|
||||
if (param === 37446) {
|
||||
return '%s';
|
||||
}
|
||||
return Reflect.apply(target, thisArg, args);
|
||||
}
|
||||
};
|
||||
|
||||
const originalGetParameter = WebGLRenderingContext.prototype.getParameter;
|
||||
WebGLRenderingContext.prototype.getParameter = new Proxy(originalGetParameter, getParameterProxyHandler);
|
||||
|
||||
const originalGetParameter2 = WebGL2RenderingContext.prototype.getParameter;
|
||||
WebGL2RenderingContext.prototype.getParameter = new Proxy(originalGetParameter2, getParameterProxyHandler);
|
||||
|
||||
// Permissions API
|
||||
const originalQuery = window.Permissions.prototype.query;
|
||||
window.Permissions.prototype.query = (parameters) => (
|
||||
parameters.name === 'notifications' ?
|
||||
Promise.resolve({ state: Notification.permission }) :
|
||||
originalQuery(parameters)
|
||||
);
|
||||
`, profile.AcceptLang, profile.Platform, profile.HardwareConc, profile.WebGLVendor, profile.WebGLRender)
|
||||
}
|
||||
|
||||
// GetBrowserProfileCount 获取浏览器配置数量
|
||||
func GetBrowserProfileCount() int {
|
||||
return len(allBrowserProfiles)
|
||||
}
|
||||
@@ -10,11 +10,15 @@ import (
|
||||
|
||||
"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) {
|
||||
// 获取随机浏览器配置
|
||||
profile := GetRandomBrowserProfile()
|
||||
|
||||
var proxyServer string
|
||||
var proxyUser string
|
||||
var proxyPass string
|
||||
@@ -36,7 +40,16 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
chromedp.Flag("no-sandbox", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("disable-blink-features", "AutomationControlled"),
|
||||
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"),
|
||||
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 != "" {
|
||||
@@ -99,9 +112,17 @@ func CompleteWithChromedp(authURL, email, password, teamID string, headless bool
|
||||
}
|
||||
})
|
||||
|
||||
// 获取反检测脚本
|
||||
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"),
|
||||
}
|
||||
|
||||
@@ -16,11 +16,12 @@ import (
|
||||
|
||||
// RodAuth 使用 Rod + Stealth 完成 OAuth 授权
|
||||
type RodAuth struct {
|
||||
browser *rod.Browser
|
||||
headless bool
|
||||
proxy string
|
||||
browser *rod.Browser
|
||||
headless bool
|
||||
proxy string
|
||||
proxyUser string
|
||||
proxyPass string
|
||||
profile BrowserProfile // 随机浏览器配置
|
||||
}
|
||||
|
||||
// getChromiumPath 获取 Chromium 路径
|
||||
@@ -53,6 +54,9 @@ func getChromiumPath() string {
|
||||
|
||||
// NewRodAuth 创建 Rod 授权器
|
||||
func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
|
||||
// 获取随机浏览器配置
|
||||
profile := GetRandomBrowserProfile()
|
||||
|
||||
var proxyServer string
|
||||
var proxyUser string
|
||||
var proxyPass string
|
||||
@@ -82,7 +86,15 @@ func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
|
||||
Set("disable-sync").
|
||||
Set("disable-translate").
|
||||
Set("metrics-recording-only").
|
||||
Set("no-first-run")
|
||||
Set("no-first-run").
|
||||
Set("disable-infobars").
|
||||
Set("disable-automation").
|
||||
// 使用随机语言和窗口大小
|
||||
Set("lang", strings.Split(profile.AcceptLang, ",")[0]).
|
||||
Set("window-size", fmt.Sprintf("%d,%d", profile.Width, profile.Height)).
|
||||
// 随机 User-Agent
|
||||
UserDataDir("").
|
||||
Set("user-agent", profile.UserAgent)
|
||||
|
||||
// 使用系统 Chromium(如果存在)
|
||||
if chromiumPath := getChromiumPath(); chromiumPath != "" {
|
||||
@@ -104,11 +116,12 @@ func NewRodAuth(headless bool, proxy string) (*RodAuth, error) {
|
||||
}
|
||||
|
||||
return &RodAuth{
|
||||
browser: browser,
|
||||
headless: headless,
|
||||
proxy: proxy,
|
||||
browser: browser,
|
||||
headless: headless,
|
||||
proxy: proxy,
|
||||
proxyUser: proxyUser,
|
||||
proxyPass: proxyPass,
|
||||
profile: profile,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -161,6 +174,23 @@ func (r *RodAuth) CompleteOAuth(authURL, email, password, teamID string) (string
|
||||
}
|
||||
defer page.Close()
|
||||
|
||||
// 设置随机窗口大小
|
||||
_ = page.SetViewport(&proto.EmulationSetDeviceMetricsOverride{
|
||||
Width: r.profile.Width,
|
||||
Height: r.profile.Height,
|
||||
DeviceScaleFactor: r.profile.PixelRatio,
|
||||
Mobile: false,
|
||||
})
|
||||
|
||||
// 注入额外的反检测脚本
|
||||
antiDetectionJS := GetAntiDetectionJS(r.profile)
|
||||
_, _ = page.Evaluate(&rod.EvalOptions{
|
||||
JS: antiDetectionJS,
|
||||
ByValue: true,
|
||||
AwaitPromise: false,
|
||||
ThisObj: nil,
|
||||
})
|
||||
|
||||
// 增加超时时间到 90 秒
|
||||
page = page.Timeout(90 * time.Second)
|
||||
|
||||
|
||||
161
backend/internal/client/fingerprints.go
Normal file
161
backend/internal/client/fingerprints.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
|
||||
"github.com/bogdanfinn/tls-client/profiles"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// 浏览器指纹配置文件
|
||||
// 整合 tls-client 高成功率指纹
|
||||
// ============================================================
|
||||
|
||||
// BrowserFingerprint 浏览器指纹完整配置
|
||||
type BrowserFingerprint struct {
|
||||
Browser string
|
||||
Version string
|
||||
Platform string
|
||||
Mobile bool
|
||||
TLSProfile profiles.ClientProfile
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Firefox 指纹池 (100% 成功率)
|
||||
// ============================================================
|
||||
|
||||
var firefoxFingerprints = []BrowserFingerprint{
|
||||
// 最新版本
|
||||
{Browser: "firefox", Version: "135", Platform: "Windows", TLSProfile: profiles.Firefox_135},
|
||||
{Browser: "firefox", Version: "135", Platform: "macOS", TLSProfile: profiles.Firefox_135},
|
||||
{Browser: "firefox", Version: "135", Platform: "Linux", TLSProfile: profiles.Firefox_135},
|
||||
{Browser: "firefox", Version: "133", Platform: "Windows", TLSProfile: profiles.Firefox_133},
|
||||
{Browser: "firefox", Version: "133", Platform: "macOS", TLSProfile: profiles.Firefox_133},
|
||||
{Browser: "firefox", Version: "133", Platform: "Linux", TLSProfile: profiles.Firefox_133},
|
||||
{Browser: "firefox", Version: "132", Platform: "Windows", TLSProfile: profiles.Firefox_132},
|
||||
{Browser: "firefox", Version: "132", Platform: "macOS", TLSProfile: profiles.Firefox_132},
|
||||
{Browser: "firefox", Version: "123", Platform: "Windows", TLSProfile: profiles.Firefox_123},
|
||||
{Browser: "firefox", Version: "120", Platform: "Windows", TLSProfile: profiles.Firefox_120},
|
||||
{Browser: "firefox", Version: "120", Platform: "macOS", TLSProfile: profiles.Firefox_120},
|
||||
{Browser: "firefox", Version: "117", Platform: "Windows", TLSProfile: profiles.Firefox_117},
|
||||
{Browser: "firefox", Version: "110", Platform: "Windows", TLSProfile: profiles.Firefox_110},
|
||||
{Browser: "firefox", Version: "108", Platform: "Windows", TLSProfile: profiles.Firefox_108},
|
||||
{Browser: "firefox", Version: "106", Platform: "Windows", TLSProfile: profiles.Firefox_106},
|
||||
{Browser: "firefox", Version: "105", Platform: "Windows", TLSProfile: profiles.Firefox_105},
|
||||
{Browser: "firefox", Version: "104", Platform: "Windows", TLSProfile: profiles.Firefox_104},
|
||||
{Browser: "firefox", Version: "102", Platform: "Windows", TLSProfile: profiles.Firefox_102},
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Safari 指纹池 (100% 成功率)
|
||||
// ============================================================
|
||||
|
||||
var safariFingerprints = []BrowserFingerprint{
|
||||
// macOS Safari
|
||||
{Browser: "safari", Version: "16.0", Platform: "macOS", TLSProfile: profiles.Safari_16_0},
|
||||
{Browser: "safari", Version: "15.6.1", Platform: "macOS", TLSProfile: profiles.Safari_15_6_1},
|
||||
// iOS Safari
|
||||
{Browser: "safari", Version: "18.5", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_18_5},
|
||||
{Browser: "safari", Version: "18.0", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_18_0},
|
||||
{Browser: "safari", Version: "17.0", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_17_0},
|
||||
{Browser: "safari", Version: "16.0", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_16_0},
|
||||
{Browser: "safari", Version: "15.6", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_15_6},
|
||||
{Browser: "safari", Version: "15.5", Platform: "iOS", Mobile: true, TLSProfile: profiles.Safari_IOS_15_5},
|
||||
// iPadOS Safari
|
||||
{Browser: "safari", Version: "15.6", Platform: "iPadOS", Mobile: true, TLSProfile: profiles.Safari_Ipad_15_6},
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Opera 指纹池 (高成功率)
|
||||
// ============================================================
|
||||
|
||||
var operaFingerprints = []BrowserFingerprint{
|
||||
{Browser: "opera", Version: "91", Platform: "Windows", TLSProfile: profiles.Opera_91},
|
||||
{Browser: "opera", Version: "90", Platform: "Windows", TLSProfile: profiles.Opera_90},
|
||||
{Browser: "opera", Version: "89", Platform: "Windows", TLSProfile: profiles.Opera_89},
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// OkHttp 指纹池 (100% 成功率 - Android 原生)
|
||||
// ============================================================
|
||||
|
||||
var okhttpFingerprints = []BrowserFingerprint{
|
||||
{Browser: "okhttp", Version: "13", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android13},
|
||||
{Browser: "okhttp", Version: "12", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android12},
|
||||
{Browser: "okhttp", Version: "11", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android11},
|
||||
{Browser: "okhttp", Version: "10", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android10},
|
||||
{Browser: "okhttp", Version: "9", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android9},
|
||||
{Browser: "okhttp", Version: "8", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android8},
|
||||
{Browser: "okhttp", Version: "7", Platform: "Android", Mobile: true, TLSProfile: profiles.Okhttp4Android7},
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// Chrome 指纹池 (测试通过的版本)
|
||||
// 注意: 新版 Chrome (120+) 在 tls-client 中被检测,只保留旧版本
|
||||
// ============================================================
|
||||
|
||||
var chromeFingerprints = []BrowserFingerprint{
|
||||
// 只保留测试通过的旧版本
|
||||
{Browser: "chrome", Version: "112", Platform: "Windows", TLSProfile: profiles.Chrome_112},
|
||||
{Browser: "chrome", Version: "111", Platform: "Windows", TLSProfile: profiles.Chrome_111},
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 合并所有指纹
|
||||
// ============================================================
|
||||
|
||||
var allFingerprints []BrowserFingerprint
|
||||
|
||||
func init() {
|
||||
allFingerprints = make([]BrowserFingerprint, 0, 100)
|
||||
|
||||
// Firefox (高优先级,多份)
|
||||
allFingerprints = append(allFingerprints, firefoxFingerprints...)
|
||||
allFingerprints = append(allFingerprints, firefoxFingerprints...)
|
||||
|
||||
// Safari (高成功率)
|
||||
allFingerprints = append(allFingerprints, safariFingerprints...)
|
||||
|
||||
// Opera
|
||||
allFingerprints = append(allFingerprints, operaFingerprints...)
|
||||
|
||||
// OkHttp (Android)
|
||||
allFingerprints = append(allFingerprints, okhttpFingerprints...)
|
||||
|
||||
// Chrome (低优先级)
|
||||
allFingerprints = append(allFingerprints, chromeFingerprints...)
|
||||
}
|
||||
|
||||
// GetRandomFingerprint 获取随机指纹
|
||||
func GetRandomFingerprint() BrowserFingerprint {
|
||||
return allFingerprints[rand.Intn(len(allFingerprints))]
|
||||
}
|
||||
|
||||
// GetRandomDesktopFingerprint 获取随机桌面端指纹
|
||||
func GetRandomDesktopFingerprint() BrowserFingerprint {
|
||||
desktopFps := make([]BrowserFingerprint, 0)
|
||||
for _, fp := range allFingerprints {
|
||||
if !fp.Mobile {
|
||||
desktopFps = append(desktopFps, fp)
|
||||
}
|
||||
}
|
||||
if len(desktopFps) == 0 {
|
||||
return allFingerprints[rand.Intn(len(allFingerprints))]
|
||||
}
|
||||
return desktopFps[rand.Intn(len(desktopFps))]
|
||||
}
|
||||
|
||||
// GetTotalFingerprintCount 获取总指纹数量
|
||||
func GetTotalFingerprintCount() int {
|
||||
return len(allFingerprints)
|
||||
}
|
||||
|
||||
// GetUniqueFingerprintCount 获取去重后的指纹数量
|
||||
func GetUniqueFingerprintCount() int {
|
||||
unique := make(map[string]bool)
|
||||
for _, fp := range allFingerprints {
|
||||
key := fp.Browser + fp.Version + fp.Platform
|
||||
unique[key] = true
|
||||
}
|
||||
return len(unique)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package client
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
@@ -14,35 +15,46 @@ import (
|
||||
"github.com/andybalholm/brotli"
|
||||
http2 "github.com/bogdanfinn/fhttp"
|
||||
tls_client "github.com/bogdanfinn/tls-client"
|
||||
"github.com/bogdanfinn/tls-client/profiles"
|
||||
)
|
||||
|
||||
// TLSClient 使用 tls-client 模拟浏览器指纹的 HTTP 客户端
|
||||
type TLSClient struct {
|
||||
client tls_client.HttpClient
|
||||
userAgent string
|
||||
chromeVer string
|
||||
acceptLang string
|
||||
client tls_client.HttpClient
|
||||
fingerprint BrowserFingerprint
|
||||
userAgent string
|
||||
acceptLang string
|
||||
}
|
||||
|
||||
// 语言偏好池
|
||||
var languagePrefs = []string{
|
||||
"en-US,en;q=0.9",
|
||||
"en-GB,en;q=0.9,en-US;q=0.8",
|
||||
"en-US,en;q=0.9,de;q=0.8",
|
||||
"de-DE,de;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"en-US,en;q=0.9,fr;q=0.8",
|
||||
"en-US,en;q=0.9,es;q=0.8",
|
||||
"fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"es-ES,es;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
}
|
||||
|
||||
// New 创建一个新的 TLS 客户端
|
||||
// New 创建一个新的 TLS 客户端(使用随机指纹)
|
||||
func New(proxyStr string) (*TLSClient, error) {
|
||||
// 获取随机桌面端指纹
|
||||
fp := GetRandomDesktopFingerprint()
|
||||
return NewWithFingerprint(fp, proxyStr)
|
||||
}
|
||||
|
||||
// NewWithFingerprint 使用指定指纹创建客户端
|
||||
func NewWithFingerprint(fp BrowserFingerprint, proxyStr string) (*TLSClient, error) {
|
||||
jar := tls_client.NewCookieJar()
|
||||
chromeVer := "133"
|
||||
|
||||
options := []tls_client.HttpClientOption{
|
||||
tls_client.WithTimeoutSeconds(60),
|
||||
tls_client.WithClientProfile(profiles.Chrome_133),
|
||||
tls_client.WithTimeoutSeconds(90),
|
||||
tls_client.WithClientProfile(fp.TLSProfile),
|
||||
tls_client.WithRandomTLSExtensionOrder(),
|
||||
tls_client.WithCookieJar(jar),
|
||||
tls_client.WithInsecureSkipVerify(),
|
||||
tls_client.WithNotFollowRedirects(),
|
||||
}
|
||||
|
||||
if proxyStr != "" {
|
||||
@@ -59,46 +71,162 @@ func New(proxyStr string) (*TLSClient, error) {
|
||||
}
|
||||
|
||||
acceptLang := languagePrefs[rand.Intn(len(languagePrefs))]
|
||||
userAgent := generateUserAgent(chromeVer)
|
||||
userAgent := generateUserAgent(fp)
|
||||
|
||||
return &TLSClient{
|
||||
client: client,
|
||||
userAgent: userAgent,
|
||||
chromeVer: chromeVer,
|
||||
acceptLang: acceptLang,
|
||||
client: client,
|
||||
fingerprint: fp,
|
||||
userAgent: userAgent,
|
||||
acceptLang: acceptLang,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateUserAgent 生成随机化的 User-Agent
|
||||
func generateUserAgent(chromeVer string) string {
|
||||
winVersions := []string{
|
||||
"Windows NT 10.0; Win64; x64",
|
||||
"Windows NT 10.0; WOW64",
|
||||
}
|
||||
winVer := winVersions[rand.Intn(len(winVersions))]
|
||||
// generateUserAgent 根据指纹生成 User-Agent
|
||||
func generateUserAgent(fp BrowserFingerprint) string {
|
||||
winVersions := []string{"Windows NT 10.0; Win64; x64", "Windows NT 11.0; Win64; x64"}
|
||||
macVersions := []string{"10_15_7", "11_0_0", "12_0_0", "13_0_0", "14_0", "14_5", "15_0", "15_2"}
|
||||
linuxVersions := []string{"X11; Linux x86_64", "X11; Ubuntu; Linux x86_64", "X11; Fedora; Linux x86_64"}
|
||||
|
||||
return "Mozilla/5.0 (" + winVer + ") AppleWebKit/537.36 (KHTML, like Gecko) Chrome/" + chromeVer + ".0.0.0 Safari/537.36"
|
||||
version := fp.Version
|
||||
|
||||
switch fp.Browser {
|
||||
case "chrome":
|
||||
switch fp.Platform {
|
||||
case "Windows":
|
||||
return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36",
|
||||
winVersions[rand.Intn(len(winVersions))], version)
|
||||
case "macOS":
|
||||
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36",
|
||||
macVersions[rand.Intn(len(macVersions))], version)
|
||||
case "Linux":
|
||||
return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36",
|
||||
linuxVersions[rand.Intn(len(linuxVersions))], version)
|
||||
case "Android":
|
||||
return fmt.Sprintf("Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Mobile Safari/537.36", version)
|
||||
}
|
||||
|
||||
case "firefox":
|
||||
switch fp.Platform {
|
||||
case "Windows":
|
||||
return fmt.Sprintf("Mozilla/5.0 (%s; rv:%s.0) Gecko/20100101 Firefox/%s.0",
|
||||
winVersions[rand.Intn(len(winVersions))], version, version)
|
||||
case "macOS":
|
||||
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s; rv:%s.0) Gecko/20100101 Firefox/%s.0",
|
||||
macVersions[rand.Intn(len(macVersions))], version, version)
|
||||
case "Linux":
|
||||
return fmt.Sprintf("Mozilla/5.0 (%s; rv:%s.0) Gecko/20100101 Firefox/%s.0",
|
||||
linuxVersions[rand.Intn(len(linuxVersions))], version, version)
|
||||
}
|
||||
|
||||
case "safari":
|
||||
if fp.Mobile {
|
||||
iosVersions := map[string]string{"18.5": "18_5", "18.0": "18_0", "17.0": "17_0", "16.0": "16_0", "15.6": "15_6", "15.5": "15_5"}
|
||||
iosVer := iosVersions[version]
|
||||
if iosVer == "" {
|
||||
iosVer = "18_0"
|
||||
}
|
||||
if fp.Platform == "iPadOS" {
|
||||
return fmt.Sprintf("Mozilla/5.0 (iPad; CPU OS %s like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Mobile/15E148 Safari/604.1", iosVer, version)
|
||||
}
|
||||
return fmt.Sprintf("Mozilla/5.0 (iPhone; CPU iPhone OS %s like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Mobile/15E148 Safari/604.1", iosVer, version)
|
||||
}
|
||||
return fmt.Sprintf("Mozilla/5.0 (Macintosh; Intel Mac OS X %s) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/%s Safari/605.1.15",
|
||||
macVersions[rand.Intn(len(macVersions))], version)
|
||||
|
||||
case "opera":
|
||||
chromeVer := map[string]string{"91": "118", "90": "117", "89": "116"}[version]
|
||||
if chromeVer == "" {
|
||||
chromeVer = "118"
|
||||
}
|
||||
return fmt.Sprintf("Mozilla/5.0 (%s) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36 OPR/%s.0.0.0",
|
||||
winVersions[rand.Intn(len(winVersions))], chromeVer, version)
|
||||
|
||||
case "okhttp":
|
||||
return "okhttp/4.12.0"
|
||||
}
|
||||
|
||||
// 默认 Chrome
|
||||
return fmt.Sprintf("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/%s.0.0.0 Safari/537.36", version)
|
||||
}
|
||||
|
||||
// getDefaultHeaders 获取默认请求头
|
||||
// getDefaultHeaders 获取默认请求头(根据浏览器类型返回不同的头)
|
||||
func (c *TLSClient) getDefaultHeaders() map[string]string {
|
||||
secChUa := `"Chromium";v="` + c.chromeVer + `", "Not(A:Brand";v="99", "Google Chrome";v="` + c.chromeVer + `"`
|
||||
|
||||
return map[string]string{
|
||||
"User-Agent": c.userAgent,
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
|
||||
"Accept-Language": c.acceptLang,
|
||||
"Accept-Encoding": "gzip, deflate, br, zstd",
|
||||
"Cache-Control": "max-age=0",
|
||||
"Sec-Ch-Ua": secChUa,
|
||||
"Sec-Ch-Ua-Mobile": "?0",
|
||||
"Sec-Ch-Ua-Platform": `"Windows"`,
|
||||
"Sec-Fetch-Dest": "document",
|
||||
"Sec-Fetch-Mode": "navigate",
|
||||
"Sec-Fetch-Site": "none",
|
||||
"Sec-Fetch-User": "?1",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
headers := map[string]string{
|
||||
"User-Agent": c.userAgent,
|
||||
"Accept-Language": c.acceptLang,
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
}
|
||||
|
||||
fp := c.fingerprint
|
||||
switch fp.Browser {
|
||||
case "firefox":
|
||||
headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"
|
||||
headers["Upgrade-Insecure-Requests"] = "1"
|
||||
headers["Sec-Fetch-Dest"] = "document"
|
||||
headers["Sec-Fetch-Mode"] = "navigate"
|
||||
headers["Sec-Fetch-Site"] = "none"
|
||||
headers["Sec-Fetch-User"] = "?1"
|
||||
headers["DNT"] = "1"
|
||||
|
||||
case "safari":
|
||||
headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
|
||||
|
||||
case "okhttp":
|
||||
headers["Accept"] = "*/*"
|
||||
headers["Accept-Encoding"] = "gzip"
|
||||
|
||||
default: // chrome, opera
|
||||
secChUa := c.generateSecChUa()
|
||||
headers["Accept"] = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7"
|
||||
headers["Cache-Control"] = "max-age=0"
|
||||
headers["Sec-Ch-Ua"] = secChUa
|
||||
headers["Sec-Ch-Ua-Mobile"] = "?0"
|
||||
if fp.Mobile {
|
||||
headers["Sec-Ch-Ua-Mobile"] = "?1"
|
||||
}
|
||||
headers["Sec-Ch-Ua-Platform"] = c.getPlatformHeader()
|
||||
headers["Sec-Fetch-Dest"] = "document"
|
||||
headers["Sec-Fetch-Mode"] = "navigate"
|
||||
headers["Sec-Fetch-Site"] = "none"
|
||||
headers["Sec-Fetch-User"] = "?1"
|
||||
headers["Upgrade-Insecure-Requests"] = "1"
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
// generateSecChUa 生成 Sec-Ch-Ua 头
|
||||
func (c *TLSClient) generateSecChUa() string {
|
||||
ver := c.fingerprint.Version
|
||||
switch c.fingerprint.Browser {
|
||||
case "opera":
|
||||
return fmt.Sprintf(`"Opera";v="%s", "Chromium";v="118", "Not(A:Brand";v="99"`, ver)
|
||||
default:
|
||||
notABrands := []string{`"Not(A:Brand";v="99"`, `"Not A(Brand";v="99"`, `"Not/A)Brand";v="99"`}
|
||||
return fmt.Sprintf(`"Chromium";v="%s", %s, "Google Chrome";v="%s"`, ver, notABrands[rand.Intn(len(notABrands))], ver)
|
||||
}
|
||||
}
|
||||
|
||||
// getPlatformHeader 获取平台头
|
||||
func (c *TLSClient) getPlatformHeader() string {
|
||||
switch c.fingerprint.Platform {
|
||||
case "macOS":
|
||||
return `"macOS"`
|
||||
case "Linux":
|
||||
return `"Linux"`
|
||||
case "iOS", "iPadOS":
|
||||
return `"iOS"`
|
||||
case "Android":
|
||||
return `"Android"`
|
||||
default:
|
||||
return `"Windows"`
|
||||
}
|
||||
}
|
||||
|
||||
// GetFingerprintInfo 获取指纹信息字符串(用于日志输出)
|
||||
func (c *TLSClient) GetFingerprintInfo() string {
|
||||
fp := c.fingerprint
|
||||
return fmt.Sprintf("%s/%s (%s)", fp.Browser, fp.Version, fp.Platform)
|
||||
}
|
||||
|
||||
// Do 执行 HTTP 请求
|
||||
|
||||
@@ -243,6 +243,43 @@ func ClearLogs() {
|
||||
logs = make([]LogEntry, 0, 1000)
|
||||
}
|
||||
|
||||
// GetLogsByModuleAndLevel 按模块和级别筛选日志并分页(最新的在前)
|
||||
func GetLogsByModuleAndLevel(module, level string, page, pageSize int) ([]LogEntry, int) {
|
||||
logsMu.RLock()
|
||||
defer logsMu.RUnlock()
|
||||
|
||||
// 倒序收集匹配的日志
|
||||
var filtered []LogEntry
|
||||
for i := len(logs) - 1; i >= 0; i-- {
|
||||
if logs[i].Module == module {
|
||||
// 如果指定了级别,则进行过滤
|
||||
if level != "" && logs[i].Level != level {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, logs[i])
|
||||
}
|
||||
}
|
||||
|
||||
total := len(filtered)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize <= 0 {
|
||||
pageSize = 5
|
||||
}
|
||||
|
||||
start := (page - 1) * pageSize
|
||||
if start >= total {
|
||||
return []LogEntry{}, total
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
|
||||
return filtered[start:end], total
|
||||
}
|
||||
|
||||
// ClearLogsByModule 按模块清除日志
|
||||
func ClearLogsByModule(module string) int {
|
||||
logsMu.Lock()
|
||||
|
||||
BIN
backend/team-reg
BIN
backend/team-reg
Binary file not shown.
Reference in New Issue
Block a user