Files
gpt-plus-gpt/pkg/captcha/capsolver.go
2026-03-15 20:48:19 +08:00

342 lines
10 KiB
Go

package captcha
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
)
const hcaptchaSolverAPI = "https://hcaptchasolver.com/api"
const defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36"
// candidateURLs to try for hCaptcha solving — b.stripecdn.com confirmed working by hcaptchasolver support.
var candidateURLs = []string{
"https://b.stripecdn.com",
"https://newassets.hcaptcha.com",
}
// Solver handles hCaptcha challenge solving via hcaptchasolver.com.
type Solver struct {
apiKey string // hcaptchasolver.com API key
proxy string
client *http.Client
StatusFn func(format string, args ...interface{}) // progress callback
}
// NewSolver creates a captcha solver.
func NewSolver(apiKey, proxy string) *Solver {
return &Solver{
apiKey: apiKey,
proxy: proxy,
client: &http.Client{Timeout: 180 * time.Second},
}
}
// SetStatusFn sets the status callback for printing progress to terminal.
func (s *Solver) SetStatusFn(fn func(format string, args ...interface{})) {
s.StatusFn = fn
}
// SetProxy updates the proxy URL.
func (s *Solver) SetProxy(proxy string) {
s.proxy = proxy
}
// statusf prints progress if StatusFn is set.
func (s *Solver) statusf(format string, args ...interface{}) {
if s.StatusFn != nil {
s.StatusFn(format, args...)
}
}
// platform represents a captcha solving service.
type platform struct {
name string
apiBase string
apiKey string
}
// platforms returns the list of solving platforms.
func (s *Solver) platforms() []platform {
var ps []platform
if s.apiKey != "" {
ps = append(ps, platform{name: "hcaptchasolver", apiBase: hcaptchaSolverAPI, apiKey: s.apiKey})
}
return ps
}
// SolveHCaptcha solves an hCaptcha challenge and returns the token + ekey.
// Uses b.stripecdn.com as the primary websiteURL (confirmed working).
func (s *Solver) SolveHCaptcha(ctx context.Context, siteKey, siteURL, rqdata string) (token, ekey string, err error) {
// Build list of URLs to try: b.stripecdn.com first (confirmed working), then user-provided, then candidates
urls := []string{}
for _, u := range candidateURLs {
urls = append(urls, u)
}
// Add user-provided URL if not already in list
if siteURL != "" {
found := false
for _, u := range urls {
if u == siteURL {
found = true
break
}
}
if !found {
urls = append(urls, siteURL)
}
}
platforms := s.platforms()
if len(platforms) == 0 {
return "", "", fmt.Errorf("no captcha solving platforms configured")
}
var lastErr error
for _, p := range platforms {
s.statusf(" → [%s] 正在解 hCaptcha...", p.name)
log.Printf("[captcha] [%s] trying...", p.name)
for i, tryURL := range urls {
log.Printf("[captcha] [%s] websiteURL %d/%d: %s", p.name, i+1, len(urls), tryURL)
token, ekey, err = s.solveWithRetry(ctx, siteKey, tryURL, rqdata, p)
if err == nil {
ekeyPreview := ekey
if len(ekeyPreview) > 16 {
ekeyPreview = ekeyPreview[:16] + "..."
}
s.statusf(" ✓ [%s] 解题成功 (token长度=%d, ekey=%s)", p.name, len(token), ekeyPreview)
log.Printf("[captcha] [%s] success!", p.name)
return token, ekey, nil
}
s.statusf(" ⚠ [%s] 失败: %v", p.name, err)
log.Printf("[captcha] [%s] failed: %v", p.name, err)
lastErr = err
if !strings.Contains(err.Error(), "UNSOLVABLE") &&
!strings.Contains(err.Error(), "CAPTCHA_FAILED") &&
!strings.Contains(err.Error(), "Bad Proxy") {
break
}
}
}
return "", "", fmt.Errorf("captcha failed on all %d platforms: %w", len(platforms), lastErr)
}
// SolveInvisibleHCaptcha solves an invisible (passive) hCaptcha challenge.
func (s *Solver) SolveInvisibleHCaptcha(ctx context.Context, siteKey, siteURL string) (token, ekey string, err error) {
log.Printf("[captcha] solving invisible hCaptcha: site_key=%s, url=%s", siteKey, siteURL)
platforms := s.platforms()
if len(platforms) == 0 {
return "", "", fmt.Errorf("no captcha solving platforms configured")
}
for _, p := range platforms {
s.statusf(" → [%s] 正在解 invisible hCaptcha...", p.name)
// Each platform gets 120s timeout (hcaptchasolver needs up to 2 min)
platformCtx, platformCancel := context.WithTimeout(ctx, 120*time.Second)
token, ekey, err = s.solveOnce(platformCtx, siteKey, siteURL, "", p, true)
platformCancel()
if err == nil {
s.statusf(" ✓ [%s] invisible 解题成功 (token长度=%d)", p.name, len(token))
log.Printf("[captcha] invisible hCaptcha solved via %s", p.name)
return token, ekey, nil
}
s.statusf(" ⚠ [%s] invisible 失败: %v", p.name, err)
log.Printf("[captcha] invisible hCaptcha failed on %s: %v", p.name, err)
}
return "", "", fmt.Errorf("invisible hCaptcha failed on all platforms: %w", err)
}
// solveWithRetry tries solving once with retry on UNSOLVABLE.
func (s *Solver) solveWithRetry(ctx context.Context, siteKey, siteURL, rqdata string, p platform) (token, ekey string, err error) {
const maxRetries = 2
for retry := 1; retry <= maxRetries; retry++ {
if retry > 1 {
log.Printf("[captcha] retry %d/%d for %s...", retry, maxRetries, siteURL)
}
token, ekey, err = s.solveOnce(ctx, siteKey, siteURL, rqdata, p, false)
if err == nil {
return token, ekey, nil
}
if !strings.Contains(err.Error(), "UNSOLVABLE") {
return "", "", err
}
log.Printf("[captcha] attempt %d failed: %v", retry, err)
}
return "", "", err
}
// solveOnce creates a task and polls for result on a single platform.
func (s *Solver) solveOnce(ctx context.Context, siteKey, siteURL, rqdata string, p platform, invisible bool) (token, ekey string, err error) {
taskType := "PopularCaptchaTaskProxyless"
if s.proxy != "" && !invisible {
taskType = "PopularCaptchaTask"
}
task := map[string]interface{}{
"type": taskType,
"websiteURL": siteURL,
"websiteKey": siteKey,
"user_agent": defaultUserAgent,
"pow_type": "hsw",
}
if rqdata != "" {
task["rqdata"] = rqdata
}
if !invisible {
task["is_invisible"] = false
}
if taskType == "PopularCaptchaTask" && s.proxy != "" {
solverProxy := s.proxy
if strings.HasPrefix(solverProxy, "socks5://") {
solverProxy = "http://" + strings.TrimPrefix(solverProxy, "socks5://")
}
task["proxy"] = solverProxy
}
taskReq := map[string]interface{}{
"clientKey": p.apiKey,
"task": task,
}
body, err := json.Marshal(taskReq)
if err != nil {
return "", "", fmt.Errorf("marshal create task: %w", err)
}
log.Printf("[captcha] [%s] createTask: %s", p.name, string(body))
req, err := http.NewRequestWithContext(ctx, "POST", p.apiBase+"/createTask", strings.NewReader(string(body)))
if err != nil {
return "", "", fmt.Errorf("create task request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.client.Do(req)
if err != nil {
return "", "", fmt.Errorf("send create task: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", fmt.Errorf("read create task response: %w", err)
}
log.Printf("[captcha] [%s] createTask response: status=%d body=%s", p.name, resp.StatusCode, string(respBody))
var createResult struct {
ErrorID int `json:"errorId"`
ErrorCode string `json:"errorCode"`
ErrorDescription string `json:"errorDescription"`
TaskID string `json:"taskId"`
}
if err := json.Unmarshal(respBody, &createResult); err != nil {
return "", "", fmt.Errorf("parse create task response: %w (body: %s)", err, string(respBody))
}
if createResult.ErrorID != 0 {
return "", "", fmt.Errorf("create task error: %s - %s", createResult.ErrorCode, createResult.ErrorDescription)
}
if createResult.TaskID == "" {
return "", "", fmt.Errorf("createTask: no taskId in response")
}
log.Printf("[captcha] [%s] task created: %s, polling for result...", p.name, createResult.TaskID)
for attempt := 1; attempt <= 60; attempt++ {
select {
case <-ctx.Done():
return "", "", ctx.Err()
case <-time.After(5 * time.Second):
}
token, ekey, done, err := s.getTaskResult(ctx, createResult.TaskID, p)
if err != nil {
return "", "", err
}
if done {
log.Printf("[captcha] [%s] solved in %d attempts (~%ds)", p.name, attempt, attempt*5)
return token, ekey, nil
}
if attempt%6 == 0 {
log.Printf("[captcha] [%s] still solving... (attempt %d, ~%ds)", p.name, attempt, attempt*5)
}
}
return "", "", fmt.Errorf("captcha solve timeout after 300s")
}
func (s *Solver) getTaskResult(ctx context.Context, taskID string, p platform) (token, ekey string, done bool, err error) {
body, err := json.Marshal(map[string]string{
"clientKey": p.apiKey,
"taskId": taskID,
})
if err != nil {
return "", "", false, fmt.Errorf("marshal get result: %w", err)
}
req, err := http.NewRequestWithContext(ctx, "POST", p.apiBase+"/getTaskResult", strings.NewReader(string(body)))
if err != nil {
return "", "", false, fmt.Errorf("create get result request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
resp, err := s.client.Do(req)
if err != nil {
return "", "", false, fmt.Errorf("send get result: %w", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return "", "", false, fmt.Errorf("read get result response: %w", err)
}
var result struct {
ErrorID int `json:"errorId"`
ErrorCode string `json:"errorCode"`
ErrorDescription string `json:"errorDescription"`
Status string `json:"status"`
Solution struct {
Token string `json:"token"`
GRecaptchaResponse string `json:"gRecaptchaResponse"`
RespKey string `json:"respKey"`
UserAgent string `json:"userAgent"`
} `json:"solution"`
}
if err := json.Unmarshal(respBody, &result); err != nil {
return "", "", false, fmt.Errorf("parse get result response: %w (body: %s)", err, string(respBody))
}
if result.ErrorID != 0 {
return "", "", false, fmt.Errorf("get result error: %s - %s", result.ErrorCode, result.ErrorDescription)
}
if result.Status == "ready" {
log.Printf("[captcha] [%s] raw result body: %s", p.name, string(respBody))
solvedToken := result.Solution.Token
if solvedToken == "" {
solvedToken = result.Solution.GRecaptchaResponse
}
log.Printf("[captcha] [%s] solved! token_len=%d, ekey=%s", p.name, len(solvedToken), result.Solution.RespKey)
return solvedToken, result.Solution.RespKey, true, nil
}
return "", "", false, nil
}