2 Commits

2 changed files with 65 additions and 26 deletions

View File

@@ -14,10 +14,10 @@ import (
const ( const (
captchaPollInterval = 1 * time.Second captchaPollInterval = 1 * time.Second
captchaTimeout = 120 * time.Second captchaPollTimeout = 120 * time.Second
) )
var captchaHTTPClient = &http.Client{Timeout: captchaTimeout} var captchaHTTPClient = &http.Client{Timeout: 5 * time.Second}
// solveCaptcha creates a task on the given captcha service and polls until it is solved, // solveCaptcha creates a task on the given captcha service and polls until it is solved,
// returning the extracted token from the solution object. // returning the extracted token from the solution object.
@@ -31,8 +31,9 @@ var captchaHTTPClient = &http.Client{Timeout: captchaTimeout}
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaKeyEmpty // - types.ErrCaptchaKeyEmpty
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func solveCaptcha(baseURL, apiKey string, task map[string]any, solutionKey string, taskIDIsString bool) (string, error) { func solveCaptcha(baseURL, apiKey string, task map[string]any, solutionKey string, taskIDIsString bool) (string, error) {
if apiKey == "" { if apiKey == "" {
@@ -52,6 +53,7 @@ func solveCaptcha(baseURL, apiKey string, task map[string]any, solutionKey strin
// //
// It can return the following errors: // It can return the following errors:
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
func captchaCreateTask(baseURL, apiKey string, task map[string]any) (string, error) { func captchaCreateTask(baseURL, apiKey string, task map[string]any) (string, error) {
body := map[string]any{ body := map[string]any{
@@ -61,7 +63,7 @@ func captchaCreateTask(baseURL, apiKey string, task map[string]any) (string, err
data, err := json.Marshal(body) data, err := json.Marshal(body)
if err != nil { if err != nil {
return "", types.NewCaptchaRequestError("createTask", err) return "", types.NewCaptchaDecodeError("createTask", err)
} }
resp, err := captchaHTTPClient.Post( resp, err := captchaHTTPClient.Post(
@@ -81,7 +83,7 @@ func captchaCreateTask(baseURL, apiKey string, task map[string]any) (string, err
TaskID json.RawMessage `json:"taskId"` TaskID json.RawMessage `json:"taskId"`
} }
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", types.NewCaptchaRequestError("createTask", err) return "", types.NewCaptchaDecodeError("createTask", err)
} }
if result.ErrorID != 0 { if result.ErrorID != 0 {
@@ -98,15 +100,15 @@ func captchaCreateTask(baseURL, apiKey string, task map[string]any) (string, err
} }
// captchaPollResult polls the getTaskResult endpoint at captchaPollInterval until the task // captchaPollResult polls the getTaskResult endpoint at captchaPollInterval until the task
// is solved, an error is returned by the service, or the overall captchaTimeout is hit. // is solved, an error is returned by the service, or the overall captchaPollTimeout is hit.
// //
// It can return the following errors: // It can return the following errors:
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaRequestError // - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func captchaPollResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsString bool) (string, error) { func captchaPollResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsString bool) (string, error) {
ctx, cancel := context.WithTimeout(context.Background(), captchaTimeout) ctx, cancel := context.WithTimeout(context.Background(), captchaPollTimeout)
defer cancel() defer cancel()
ticker := time.NewTicker(captchaPollInterval) ticker := time.NewTicker(captchaPollInterval)
@@ -115,12 +117,18 @@ func captchaPollResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsStri
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return "", types.NewCaptchaTimeoutError(taskID) return "", types.NewCaptchaPollTimeoutError(taskID)
case <-ticker.C: case <-ticker.C:
token, err := captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey, taskIDIsString) token, err := captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey, taskIDIsString)
if errors.Is(err, types.ErrCaptchaProcessing) { if errors.Is(err, types.ErrCaptchaProcessing) {
continue continue
} }
// Retry on transient HTTP errors (timeouts, connection resets, etc.)
// instead of failing the entire solve. The poll loop timeout will
// eventually catch permanently unreachable services.
if _, ok := errors.AsType[types.CaptchaRequestError](err); ok {
continue
}
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -134,6 +142,7 @@ func captchaPollResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsStri
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaProcessing // - types.ErrCaptchaProcessing
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsString bool) (string, error) { func captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsString bool) (string, error) {
@@ -146,7 +155,7 @@ func captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsS
data, err := json.Marshal(bodyMap) data, err := json.Marshal(bodyMap)
if err != nil { if err != nil {
return "", types.NewCaptchaRequestError("getTaskResult", err) return "", types.NewCaptchaDecodeError("getTaskResult", err)
} }
resp, err := captchaHTTPClient.Post( resp, err := captchaHTTPClient.Post(
@@ -167,7 +176,7 @@ func captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsS
Solution map[string]any `json:"solution"` Solution map[string]any `json:"solution"`
} }
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", types.NewCaptchaRequestError("getTaskResult", err) return "", types.NewCaptchaDecodeError("getTaskResult", err)
} }
if result.ErrorID != 0 { if result.ErrorID != 0 {
@@ -199,8 +208,9 @@ const twoCaptchaBaseURL = "https://api.2captcha.com"
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaKeyEmpty // - types.ErrCaptchaKeyEmpty
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func twoCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) { func twoCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) {
return solveCaptcha(twoCaptchaBaseURL, apiKey, map[string]any{ return solveCaptcha(twoCaptchaBaseURL, apiKey, map[string]any{
@@ -216,8 +226,9 @@ func twoCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string,
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaKeyEmpty // - types.ErrCaptchaKeyEmpty
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func twoCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) { func twoCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) {
task := map[string]any{ task := map[string]any{
@@ -237,8 +248,9 @@ func twoCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction strin
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaKeyEmpty // - types.ErrCaptchaKeyEmpty
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func twoCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) { func twoCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) {
task := map[string]any{ task := map[string]any{
@@ -261,8 +273,9 @@ const antiCaptchaBaseURL = "https://api.anti-captcha.com"
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaKeyEmpty // - types.ErrCaptchaKeyEmpty
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func antiCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) { func antiCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) {
return solveCaptcha(antiCaptchaBaseURL, apiKey, map[string]any{ return solveCaptcha(antiCaptchaBaseURL, apiKey, map[string]any{
@@ -279,8 +292,9 @@ func antiCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string,
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaKeyEmpty // - types.ErrCaptchaKeyEmpty
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func antiCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) { func antiCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) {
task := map[string]any{ task := map[string]any{
@@ -301,8 +315,9 @@ func antiCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction stri
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaKeyEmpty // - types.ErrCaptchaKeyEmpty
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func antiCaptchaSolveHCaptcha(apiKey, websiteURL, websiteKey string) (string, error) { func antiCaptchaSolveHCaptcha(apiKey, websiteURL, websiteKey string) (string, error) {
return solveCaptcha(antiCaptchaBaseURL, apiKey, map[string]any{ return solveCaptcha(antiCaptchaBaseURL, apiKey, map[string]any{
@@ -318,8 +333,9 @@ func antiCaptchaSolveHCaptcha(apiKey, websiteURL, websiteKey string) (string, er
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaKeyEmpty // - types.ErrCaptchaKeyEmpty
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func antiCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) { func antiCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) {
task := map[string]any{ task := map[string]any{
@@ -342,8 +358,9 @@ const capSolverBaseURL = "https://api.capsolver.com"
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaKeyEmpty // - types.ErrCaptchaKeyEmpty
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func capSolverSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) { func capSolverSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) {
return solveCaptcha(capSolverBaseURL, apiKey, map[string]any{ return solveCaptcha(capSolverBaseURL, apiKey, map[string]any{
@@ -359,8 +376,9 @@ func capSolverSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, e
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaKeyEmpty // - types.ErrCaptchaKeyEmpty
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func capSolverSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) { func capSolverSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) {
task := map[string]any{ task := map[string]any{
@@ -380,8 +398,9 @@ func capSolverSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string
// It can return the following errors: // It can return the following errors:
// - types.ErrCaptchaKeyEmpty // - types.ErrCaptchaKeyEmpty
// - types.CaptchaRequestError // - types.CaptchaRequestError
// - types.CaptchaDecodeError
// - types.CaptchaAPIError // - types.CaptchaAPIError
// - types.CaptchaTimeoutError // - types.CaptchaPollTimeoutError
// - types.CaptchaSolutionKeyError // - types.CaptchaSolutionKeyError
func capSolverSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) { func capSolverSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) {
task := map[string]any{ task := map[string]any{

View File

@@ -520,15 +520,35 @@ func (e CaptchaRequestError) Unwrap() error {
return e.Err return e.Err
} }
type CaptchaTimeoutError struct { type CaptchaDecodeError struct {
Endpoint string
Err error
}
func NewCaptchaDecodeError(endpoint string, err error) CaptchaDecodeError {
if err == nil {
err = errNoError
}
return CaptchaDecodeError{Endpoint: endpoint, Err: err}
}
func (e CaptchaDecodeError) Error() string {
return fmt.Sprintf("captcha %s decode failed: %v", e.Endpoint, e.Err)
}
func (e CaptchaDecodeError) Unwrap() error {
return e.Err
}
type CaptchaPollTimeoutError struct {
TaskID string TaskID string
} }
func NewCaptchaTimeoutError(taskID string) CaptchaTimeoutError { func NewCaptchaPollTimeoutError(taskID string) CaptchaPollTimeoutError {
return CaptchaTimeoutError{TaskID: taskID} return CaptchaPollTimeoutError{TaskID: taskID}
} }
func (e CaptchaTimeoutError) Error() string { func (e CaptchaPollTimeoutError) Error() string {
return fmt.Sprintf("captcha solving timed out (taskId: %s)", e.TaskID) return fmt.Sprintf("captcha solving timed out (taskId: %s)", e.TaskID)
} }