Initial sanitized code sync
This commit is contained in:
341
pkg/captcha/capsolver.go
Normal file
341
pkg/captcha/capsolver.go
Normal file
@@ -0,0 +1,341 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user