Files
xiaolajiaoyyds 6f71df4849 Initial open-source release
多平台账号批量注册系统,支持 OpenAI / Grok / Kiro / Gemini 平台的全自动注册。

Features:
- 用户名/密码注册登录(首个用户自动管理员)
- JWT 认证 + bcrypt 密码加密
- 积分系统与多用户管理
- HuggingFace Space 弹性 Worker 节点管理
- Cloudflare Worker 负载均衡
- 实时日志推送(SSE/WebSocket)
- 管理后台(用户管理、系统设置、代理池、卡池)

Tech Stack: Go 1.25 / Gin / GORM / PostgreSQL / Vue 3 / Vite / TailwindCSS
2026-03-22 03:44:42 +08:00

657 lines
18 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 turnstile
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"sync"
"time"
)
// ── 健康追踪 ──
// solverHealth 单个 solver 节点的健康状态
type solverHealth struct {
mu sync.Mutex
consecutiveFail int
cooldownUntil time.Time
}
// record 记录一次求解结果
func (h *solverHealth) record(ok bool) {
h.mu.Lock()
defer h.mu.Unlock()
if ok {
h.consecutiveFail = 0
h.cooldownUntil = time.Time{}
} else {
h.consecutiveFail++
// 连续 3 次失败 → 冷却 2 分钟
if h.consecutiveFail >= 3 {
h.cooldownUntil = time.Now().Add(2 * time.Minute)
}
}
}
// isHealthy 是否可用(未在冷却中)
func (h *solverHealth) isHealthy() bool {
h.mu.Lock()
defer h.mu.Unlock()
return h.cooldownUntil.IsZero() || time.Now().After(h.cooldownUntil)
}
// ── Solver ──
// Solver Cloudflare Turnstile 验证码求解器(多后端并行竞速)
type Solver struct {
solverURLs []string // 本地 solver URL 列表
capSolverKey string // CapSolver API key
yesCaptchaKey string // YesCaptcha API key
proxyURL string // 代理地址(传给本地 solver 浏览器)
httpClient *http.Client
// 健康追踪(按 solver URL / 服务名 索引)
healthMu sync.RWMutex
health map[string]*solverHealth
}
// NewSolver 创建求解器
func NewSolver(solverURLs []string, capSolverKey, yesCaptchaKey, proxyURL string) *Solver {
cleaned := make([]string, 0, len(solverURLs))
for _, u := range solverURLs {
u = strings.TrimSpace(u)
u = strings.TrimRight(u, "/")
if u != "" {
cleaned = append(cleaned, u)
}
}
return &Solver{
solverURLs: cleaned,
capSolverKey: capSolverKey,
yesCaptchaKey: yesCaptchaKey,
proxyURL: proxyURL,
httpClient: &http.Client{Timeout: 30 * time.Second},
health: make(map[string]*solverHealth),
}
}
// getHealth 获取指定 solver 的健康状态(懒初始化)
func (s *Solver) getHealth(key string) *solverHealth {
s.healthMu.RLock()
h, ok := s.health[key]
s.healthMu.RUnlock()
if ok {
return h
}
s.healthMu.Lock()
defer s.healthMu.Unlock()
if h, ok = s.health[key]; !ok {
h = &solverHealth{}
s.health[key] = h
}
return h
}
// ── 主入口 ──
// Solve 求解 Turnstile,返回 token
// 级联顺序:本地 solver 并行竞速 → 云端 solver 并行竞速 → 内置脚本
func (s *Solver) Solve(ctx context.Context, siteURL, siteKey string, maxAttempts int, logf func(string, ...interface{})) (string, error) {
if logf == nil {
logf = func(string, ...interface{}) {}
}
var lastErr error
for i := 0; i < maxAttempts; i++ {
if ctx.Err() != nil {
return "", ctx.Err()
}
// 1) 本地 solver 并行竞速
if len(s.solverURLs) > 0 {
token, err := s.raceLocalSolvers(ctx, siteURL, siteKey, logf)
if err == nil && token != "" {
return token, nil
}
lastErr = err
}
// 2) 云端 solver 并行竞速(CapSolver + YesCaptcha 同时提交)
if s.capSolverKey != "" || s.yesCaptchaKey != "" {
token, err := s.raceCloudSolvers(ctx, siteURL, siteKey, logf)
if err == nil && token != "" {
return token, nil
}
lastErr = err
}
// 3) 内置脚本(最后手段)
if ctx.Err() != nil {
return "", ctx.Err()
}
logf("[*] 切换到内置脚本...")
if token, err := s.solveBuiltinScript(ctx, siteURL, siteKey); err == nil && token != "" {
logf("[+] 卧槽Σ(°ロ°)成功了(内置脚本)")
return token, nil
}
if i < maxAttempts-1 {
logf("[!] 本轮全部失败,等待重试 (%d/%d)...", i+1, maxAttempts)
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(3 * time.Second):
}
}
}
return "", fmt.Errorf("验证求解失败 (%d 次): %v", maxAttempts, lastErr)
}
// ── 并行竞速 ──
// raceLocalSolvers 并行提交到所有本地 solver,第一个成功的 token 胜出
func (s *Solver) raceLocalSolvers(ctx context.Context, siteURL, siteKey string, logf func(string, ...interface{})) (string, error) {
// 单 solver 直接串行(省 goroutine 开销)
if len(s.solverURLs) == 1 {
u := s.solverURLs[0]
h := s.getHealth(u)
logf("[*] 冲鸭冲鸭冲鸭...")
token, err := s.solveLocal(ctx, u, siteURL, siteKey, logf)
if err == nil && token != "" {
logf("[+] 卧槽Σ(°ロ°)成功了 #1")
h.record(true)
return token, nil
}
h.record(false)
logf("[-] 解密中 #1 失败: %v", err)
return "", err
}
// 多 solver 并行竞速
raceCtx, raceCancel := context.WithCancel(ctx)
defer raceCancel()
type raceResult struct {
token string
err error
idx int
}
ch := make(chan raceResult, len(s.solverURLs))
for idx, solverURL := range s.solverURLs {
idx, solverURL := idx, solverURL
h := s.getHealth(solverURL)
go func() {
// 冷却中的 solver 延迟启动(仍参与竞速,只是让健康的先跑)
if !h.isHealthy() {
logf("[*] 解密中 #%d 冷却中,延迟启动...", idx+1)
select {
case <-raceCtx.Done():
ch <- raceResult{err: raceCtx.Err(), idx: idx}
return
case <-time.After(5 * time.Second):
}
}
logf("[*] 冲鸭冲鸭冲鸭... #%d", idx+1)
token, err := s.solveLocal(raceCtx, solverURL, siteURL, siteKey, logf)
ch <- raceResult{token: token, err: err, idx: idx}
}()
}
var lastErr error
for range s.solverURLs {
select {
case <-ctx.Done():
return "", ctx.Err()
case r := <-ch:
h := s.getHealth(s.solverURLs[r.idx])
if r.err == nil && r.token != "" {
logf("[+] 卧槽Σ(°ロ°)成功了 #%d", r.idx+1)
raceCancel() // 取消其他 solver
h.record(true)
return r.token, nil
}
// 被取消的不算失败
if r.err != nil && r.err != context.Canceled {
logf("[-] 解密中 #%d 失败: %v", r.idx+1, r.err)
lastErr = r.err
h.record(false)
}
}
}
return "", lastErr
}
// raceCloudSolvers 并行提交 CapSolver + YesCaptcha,第一个成功的胜出
func (s *Solver) raceCloudSolvers(ctx context.Context, siteURL, siteKey string, logf func(string, ...interface{})) (string, error) {
raceCtx, raceCancel := context.WithCancel(ctx)
defer raceCancel()
type raceResult struct {
token string
err error
name string
}
var count int
ch := make(chan raceResult, 2)
if s.capSolverKey != "" {
count++
go func() {
logf("[*] 冲鸭冲鸭冲鸭... 云端A")
token, err := s.solveCapSolver(raceCtx, siteURL, siteKey, logf)
ch <- raceResult{token: token, err: err, name: "云端A"}
}()
}
if s.yesCaptchaKey != "" {
count++
go func() {
logf("[*] 冲鸭冲鸭冲鸭... 云端B")
token, err := s.solveYesCaptcha(raceCtx, siteURL, siteKey, logf)
ch <- raceResult{token: token, err: err, name: "云端B"}
}()
}
if count == 0 {
return "", fmt.Errorf("无可用云端服务")
}
var lastErr error
for j := 0; j < count; j++ {
select {
case <-ctx.Done():
return "", ctx.Err()
case r := <-ch:
if r.err == nil && r.token != "" {
logf("[+] 卧槽Σ(°ロ°)成功了(%s)", r.name)
raceCancel()
return r.token, nil
}
if r.err != nil && r.err != context.Canceled {
logf("[-] %s 失败: %v", r.name, r.err)
lastErr = r.err
}
}
}
return "", lastErr
}
// ── 远程端 ──
// solveLocal 单节点本地求解(提交 + 轮询)
func (s *Solver) solveLocal(ctx context.Context, solverURL, siteURL, siteKey string, logf func(string, ...interface{})) (string, error) {
// 提交请求 — 使用 10s 超时快速检测不可达节点
submitCtx, submitCancel := context.WithTimeout(ctx, 10*time.Second)
defer submitCancel()
reqURL := fmt.Sprintf("%s/turnstile?url=%s&sitekey=%s", solverURL, url.QueryEscape(siteURL), url.QueryEscape(siteKey))
if s.proxyURL != "" {
reqURL += "&proxy=" + url.QueryEscape(s.proxyURL)
}
req, err := http.NewRequestWithContext(submitCtx, "GET", reqURL, nil)
if err != nil {
return "", fmt.Errorf("远程端构建请求失败: %w", err)
}
resp, err := s.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("远程端连接失败: %w", err)
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var createResp map[string]interface{}
if err := json.Unmarshal(body, &createResp); err != nil {
return "", fmt.Errorf("远程端响应解析失败: %w", err)
}
taskID, _ := createResp["taskId"].(string)
if taskID == "" {
taskID, _ = createResp["task_id"].(string)
}
if taskID == "" {
if token, ok := createResp["token"].(string); ok && token != "" {
return token, nil
}
return "", fmt.Errorf("远程端无 taskId")
}
logf("[*] 任务提交啦,等等我马上就好...")
// 首次轮询等待缩短到 2s(原 5s 太慢,浏览器启动 + 页面加载通常 2-3s)
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(2 * time.Second):
}
resultURL := fmt.Sprintf("%s/result?id=%s", solverURL, taskID)
for j := 0; j < 30; j++ {
if ctx.Err() != nil {
return "", ctx.Err()
}
pollReq, _ := http.NewRequestWithContext(ctx, "GET", resultURL, nil)
resp, err := s.httpClient.Do(pollReq)
if err != nil {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(2 * time.Second):
}
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(2 * time.Second):
}
continue
}
if errID, ok := result["errorId"].(float64); ok && errID != 0 {
desc, _ := result["errorDescription"].(string)
code, _ := result["errorCode"].(string)
// "Task not found" = poll 打到了负载均衡的错误节点,继续轮询等正确节点
if strings.Contains(strings.ToLower(desc), "not found") || strings.Contains(strings.ToLower(code), "not_found") {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(2 * time.Second):
}
continue
}
return "", fmt.Errorf("远程端失败: %v (%s)", code, desc)
}
status, _ := result["status"].(string)
if status == "CAPTCHA_FAIL" {
return "", fmt.Errorf("远程端失败")
}
if j%3 == 0 {
logf("[*] 等等我马上就好... (%d/30)", j+1)
}
if solution, ok := result["solution"].(map[string]interface{}); ok {
if token, ok := solution["token"].(string); ok && token != "" {
return token, nil
}
}
if token, ok := result["token"].(string); ok && token != "" {
return token, nil
}
if value, ok := result["value"].(string); ok && value != "" {
return value, nil
}
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(2 * time.Second):
}
}
return "", fmt.Errorf("远程端轮询超时")
}
// ── 云端求解器 ──
// solveCapSolver CapSolver 云服务求解 Turnstile
func (s *Solver) solveCapSolver(ctx context.Context, siteURL, siteKey string, logf func(string, ...interface{})) (string, error) {
// 创建任务
payload, _ := json.Marshal(map[string]interface{}{
"clientKey": s.capSolverKey,
"task": map[string]interface{}{
"type": "AntiTurnstileTaskProxyLess",
"websiteURL": siteURL,
"websiteKey": siteKey,
},
})
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.capsolver.com/createTask", strings.NewReader(string(payload)))
if err != nil {
return "", fmt.Errorf("CapSolver 构建请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("CapSolver 连接失败: %w", err)
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var createResp struct {
ErrorID int `json:"errorId"`
ErrorCode string `json:"errorCode"`
ErrorDescription string `json:"errorDescription"`
TaskID string `json:"taskId"`
}
if err := json.Unmarshal(body, &createResp); err != nil {
return "", fmt.Errorf("CapSolver 响应解析失败: %w", err)
}
if createResp.ErrorID != 0 {
return "", fmt.Errorf("CapSolver 创建任务失败: %s (%s)", createResp.ErrorCode, createResp.ErrorDescription)
}
if createResp.TaskID == "" {
return "", fmt.Errorf("CapSolver 无 taskId: %s", string(body))
}
logf("[*] 云端A 任务提交啦,等等我...")
// 轮询结果(最多 120 秒)
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(5 * time.Second):
}
for j := 0; j < 40; j++ {
if ctx.Err() != nil {
return "", ctx.Err()
}
pollPayload, _ := json.Marshal(map[string]interface{}{
"clientKey": s.capSolverKey,
"taskId": createResp.TaskID,
})
pollReq, _ := http.NewRequestWithContext(ctx, "POST", "https://api.capsolver.com/getTaskResult", strings.NewReader(string(pollPayload)))
pollReq.Header.Set("Content-Type", "application/json")
resp, err := s.httpClient.Do(pollReq)
if err != nil {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(3 * time.Second):
}
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var result struct {
ErrorID int `json:"errorId"`
Status string `json:"status"`
Solution struct {
Token string `json:"token"`
UserAgent string `json:"userAgent"`
} `json:"solution"`
}
if err := json.Unmarshal(body, &result); err != nil {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(3 * time.Second):
}
continue
}
if result.Status == "ready" && result.Solution.Token != "" {
return result.Solution.Token, nil
}
if result.ErrorID != 0 {
return "", fmt.Errorf("CapSolver 任务失败: errorId=%d", result.ErrorID)
}
if result.Status == "failed" {
return "", fmt.Errorf("CapSolver 任务失败")
}
if j%5 == 0 {
logf("[*] 云端A 等等我马上就好... (%d/40)", j+1)
}
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(3 * time.Second):
}
}
return "", fmt.Errorf("CapSolver 轮询超时")
}
// solveYesCaptcha YesCaptcha 云服务求解
func (s *Solver) solveYesCaptcha(ctx context.Context, siteURL, siteKey string, logf func(string, ...interface{})) (string, error) {
payload, _ := json.Marshal(map[string]interface{}{
"clientKey": s.yesCaptchaKey,
"task": map[string]interface{}{
"type": "TurnstileTaskProxyless",
"websiteURL": siteURL,
"websiteKey": siteKey,
},
})
req, err := http.NewRequestWithContext(ctx, "POST", "https://api.yescaptcha.com/createTask", strings.NewReader(string(payload)))
if err != nil {
return "", fmt.Errorf("YesCaptcha 构建请求失败: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.httpClient.Do(req)
if err != nil {
return "", fmt.Errorf("YesCaptcha 连接失败: %w", err)
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var createResp map[string]interface{}
if err := json.Unmarshal(body, &createResp); err != nil {
return "", fmt.Errorf("YesCaptcha 响应解析失败: %w", err)
}
taskID, ok := createResp["taskId"].(string)
if !ok || taskID == "" {
return "", fmt.Errorf("YesCaptcha 无 taskId: %s", string(body))
}
logf("[*] 云端B 任务提交啦,等等我...")
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(5 * time.Second):
}
for j := 0; j < 60; j++ {
if ctx.Err() != nil {
return "", ctx.Err()
}
pollPayload, _ := json.Marshal(map[string]interface{}{
"clientKey": s.yesCaptchaKey,
"taskId": taskID,
})
pollReq, _ := http.NewRequestWithContext(ctx, "POST", "https://api.yescaptcha.com/getTaskResult", strings.NewReader(string(pollPayload)))
pollReq.Header.Set("Content-Type", "application/json")
resp, err := s.httpClient.Do(pollReq)
if err != nil {
logf("[*] 云端B 请求失败,重试中...", )
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(3 * time.Second):
}
continue
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(3 * time.Second):
}
continue
}
status, _ := result["status"].(string)
if j%5 == 0 {
logf("[*] 云端B 等等我马上就好... (%d/60)", j+1)
}
if status == "ready" {
if solution, ok := result["solution"].(map[string]interface{}); ok {
if token, ok := solution["token"].(string); ok && token != "" {
return token, nil
}
}
}
if status == "failed" {
return "", fmt.Errorf("YesCaptcha 任务失败")
}
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(3 * time.Second):
}
}
return "", fmt.Errorf("YesCaptcha 轮询超时")
}
// ── 内置脚本 ──
// solveBuiltinScript 内置浏览器脚本求解(最后手段)
func (s *Solver) solveBuiltinScript(ctx context.Context, siteURL, siteKey string) (string, error) {
scriptPath := os.Getenv("TURNSTILE_SOLVER_SCRIPT")
if scriptPath == "" {
scriptPath = os.Getenv("CAMOUFOX_SOLVER_SCRIPT")
}
if scriptPath == "" {
execDir := filepath.Dir(os.Args[0])
candidates := []string{
"scripts/solve_turnstile.py",
filepath.Join(execDir, "scripts", "solve_turnstile.py"),
filepath.Join(execDir, "..", "scripts", "solve_turnstile.py"),
}
for _, p := range candidates {
if _, err := os.Stat(p); err == nil {
scriptPath = p
break
}
}
}
if scriptPath == "" {
return "", fmt.Errorf("内置 solver 脚本未找到")
}
scriptCtx, cancel := context.WithTimeout(ctx, 90*time.Second)
defer cancel()
cmd := exec.CommandContext(scriptCtx, "python3", scriptPath, siteURL, siteKey, "45")
output, err := cmd.CombinedOutput()
if err != nil {
return "", fmt.Errorf("内置 solver 失败: %w\n%s", err, string(output))
}
for _, line := range strings.Split(string(output), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "TOKEN:") {
return strings.TrimPrefix(line, "TOKEN:"), nil
}
if strings.HasPrefix(line, "ERROR:") {
return "", fmt.Errorf("内置 solver: %s", strings.TrimPrefix(line, "ERROR:"))
}
}
return "", fmt.Errorf("内置 solver 无 token")
}