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 }