mirror of
https://github.com/aykhans/sarin.git
synced 2026-04-14 12:09:36 +00:00
Compare commits
4 Commits
006029aad1
...
cea692cf1b
| Author | SHA1 | Date | |
|---|---|---|---|
| cea692cf1b | |||
| 88f5171132 | |||
| 16b0081d3e | |||
| 1bd58a02b7 |
@@ -26,10 +26,11 @@ Sarin is designed for efficient HTTP load testing with minimal resource consumpt
|
||||
| ---------------------------------------------------------- | ------------------------------- |
|
||||
| High-performance with low memory footprint | Detailed response body analysis |
|
||||
| Long-running duration/count based tests | Extensive response statistics |
|
||||
| Dynamic requests via 320+ template functions | Web UI or complex TUI |
|
||||
| Dynamic requests via 340+ template functions | Web UI or complex TUI |
|
||||
| Request scripting with Lua and JavaScript | Distributed load testing |
|
||||
| Multiple proxy protocols<br>(HTTP, HTTPS, SOCKS5, SOCKS5H) | HTTP/2, HTTP/3, WebSocket, gRPC |
|
||||
| Flexible config (CLI, ENV, YAML) | Plugins / extensions ecosystem |
|
||||
| Captcha solving<br>(2Captcha, Anti-Captcha, CapSolver) | Plugins / extensions ecosystem |
|
||||
| Flexible config (CLI, ENV, YAML) | |
|
||||
|
||||
## Installation
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ This guide provides practical examples for common Sarin use cases.
|
||||
- [Request-Based vs Duration-Based Tests](#request-based-vs-duration-based-tests)
|
||||
- [Headers, Cookies, and Parameters](#headers-cookies-and-parameters)
|
||||
- [Dynamic Requests with Templating](#dynamic-requests-with-templating)
|
||||
- [Solving Captchas](#solving-captchas)
|
||||
- [Request Bodies](#request-bodies)
|
||||
- [File Uploads](#file-uploads)
|
||||
- [Using Proxies](#using-proxies)
|
||||
@@ -371,7 +372,29 @@ body: '{"ip": "{{ fakeit_IPv4Address }}", "timestamp": "{{ fakeit_Date }}", "act
|
||||
|
||||
</details>
|
||||
|
||||
> For the complete list of 320+ template functions, see the **[Templating Guide](templating.md)**.
|
||||
> For the complete list of 340+ template functions, see the **[Templating Guide](templating.md)**.
|
||||
|
||||
## Solving Captchas
|
||||
|
||||
Sarin can solve captchas through third-party services and embed the resulting token into the request. Three services are supported via dedicated template functions: **2Captcha**, **Anti-Captcha**, and **CapSolver**.
|
||||
|
||||
**Solve a reCAPTCHA v2 and submit the token in the request body:**
|
||||
|
||||
```sh
|
||||
sarin -U https://example.com/login -M POST -r 1 \
|
||||
-B '{"g-recaptcha-response": "{{ twocaptcha_RecaptchaV2 "YOUR_API_KEY" "SITE_KEY" "https://example.com/login" }}"}'
|
||||
```
|
||||
|
||||
**Reuse a single solved token across multiple requests via `values`:**
|
||||
|
||||
```sh
|
||||
sarin -U https://example.com/api -M POST -r 5 \
|
||||
-V 'TOKEN={{ anticaptcha_Turnstile "YOUR_API_KEY" "SITE_KEY" "https://example.com/api" }}' \
|
||||
-H "X-Turnstile-Token: {{ .Values.TOKEN }}" \
|
||||
-B '{"token": "{{ .Values.TOKEN }}"}'
|
||||
```
|
||||
|
||||
> See the **[Templating Guide](templating.md#captcha-functions)** for the full list of captcha functions and per-service support.
|
||||
|
||||
## Request Bodies
|
||||
|
||||
|
||||
@@ -4,16 +4,23 @@ Sarin supports Go templates in URL paths, methods, bodies, headers, params, cook
|
||||
|
||||
> **Note:** Templating in URL host and scheme is not supported. Only the path portion of the URL can contain templates.
|
||||
|
||||
> **Note:** Template rendering happens before the request is sent. The request timeout (`-T` / `timeout`) only governs the HTTP request itself and starts _after_ templates have finished rendering, so slow template functions (e.g. captcha solvers, remote `file_Read`) cannot cause a request timeout no matter how long they take.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Using Values](#using-values)
|
||||
- [General Functions](#general-functions)
|
||||
- [String Functions](#string-functions)
|
||||
- [Collection Functions](#collection-functions)
|
||||
- [JSON Functions](#json-functions)
|
||||
- [Time Functions](#time-functions)
|
||||
- [Crypto Functions](#crypto-functions)
|
||||
- [Body Functions](#body-functions)
|
||||
- [File Functions](#file-functions)
|
||||
- [Captcha Functions](#captcha-functions)
|
||||
- [2Captcha](#2captcha)
|
||||
- [Anti-Captcha](#anti-captcha)
|
||||
- [CapSolver](#capsolver)
|
||||
- [Fake Data Functions](#fake-data-functions)
|
||||
- [File](#file)
|
||||
- [ID](#id)
|
||||
@@ -111,6 +118,33 @@ sarin -U http://example.com/users \
|
||||
| `slice_Int(values ...int)` | Create int slice | `{{ slice_Int 1 2 3 }}` |
|
||||
| `slice_Uint(values ...uint)` | Create uint slice | `{{ slice_Uint 1 2 3 }}` |
|
||||
|
||||
### JSON Functions
|
||||
|
||||
Build JSON payloads programmatically without manual quoting or escaping. `json_Object` is the ergonomic shortcut for flat objects; `json_Encode` marshals any value (slice, map, etc.) to a JSON string.
|
||||
|
||||
| Function | Description | Example |
|
||||
| --------------------------- | ------------------------------------------------------------------------------------------------------ | ----------------------------------------------------- |
|
||||
| `json_Object(pairs ...any)` | Build an object from interleaved key-value pairs and return it as a JSON string. Keys must be strings. | `{{ json_Object "name" "Alice" "age" 30 }}` |
|
||||
| `json_Encode(v any)` | Marshal any value (slice, map, etc.) to a JSON string. | `{{ json_Encode (slice_Str "a" "b") }}` → `["a","b"]` |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```yaml
|
||||
# Flat object with fake data
|
||||
body: '{{ json_Object "name" (fakeit_FirstName) "email" (fakeit_Email) }}'
|
||||
|
||||
# Embed a solved captcha token
|
||||
body: '{{ json_Object "g-recaptcha-response" (twocaptcha_RecaptchaV2 "API_KEY" "SITE_KEY" "https://example.com") }}'
|
||||
|
||||
# Encode a slice as a JSON array
|
||||
body: '{{ json_Encode (slice_Str "a" "b" "c") }}'
|
||||
|
||||
# Encode a string dictionary (map[string]string)
|
||||
body: '{{ json_Encode (dict_Str "key1" "value1" "key2" "value2") }}'
|
||||
```
|
||||
|
||||
> **Note:** Object keys are serialized in alphabetical order (Go's `encoding/json` default), not insertion order. For API payloads this is almost always fine because JSON key order is semantically irrelevant.
|
||||
|
||||
### Time Functions
|
||||
|
||||
| Function | Description | Example |
|
||||
@@ -196,6 +230,95 @@ values: "FILE_DATA={{ file_Base64 \"/path/to/file.bin\" }}"
|
||||
body: '{"data": "{{ .Values.FILE_DATA }}"}'
|
||||
```
|
||||
|
||||
## Captcha Functions
|
||||
|
||||
Captcha functions solve a captcha challenge through a third-party solving service and return the resulting token, which can then be embedded directly into a request. They are intended for load testing endpoints protected by reCAPTCHA, hCaptcha, or Cloudflare Turnstile.
|
||||
|
||||
The functions are organized by service: `twocaptcha_*`, `anticaptcha_*`, and `capsolver_*`. Each accepts the API key as the first argument so no global configuration is required — bring your own key and use any of the supported services per template.
|
||||
|
||||
> **Important — performance and cost:**
|
||||
>
|
||||
> - **Each call is slow.** Solving typically takes ~5–60 seconds because the function blocks the template render until the third-party service returns a token. Internally the solver polls every 1s and gives up after 120s.
|
||||
> - **Each call costs money.** Every successful solve is billed by the captcha service (typically $0.001–$0.003 per solve). For high-volume tests, your captcha bill grows linearly with request count.
|
||||
|
||||
**Common parameters across all captcha functions:**
|
||||
|
||||
- `apiKey` - Your API key for the chosen captcha solving service
|
||||
- `siteKey` - The captcha sitekey extracted from the target page (e.g. the `data-sitekey` attribute on a reCAPTCHA, hCaptcha, or Turnstile element)
|
||||
- `pageURL` - The URL of the page where the captcha is hosted
|
||||
|
||||
### 2Captcha
|
||||
|
||||
Functions for the [2Captcha](https://2captcha.com) service. Note: 2Captcha **does not currently support hCaptcha** through their API.
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------------------------------------------------------ | ------------------------------------------------------------------------- |
|
||||
| `twocaptcha_RecaptchaV2(apiKey, siteKey, pageURL string)` | Solve a Google reCAPTCHA v2 challenge |
|
||||
| `twocaptcha_RecaptchaV3(apiKey, siteKey, pageURL, pageAction string)` | Solve a Google reCAPTCHA v3 challenge. Pass `""` for `pageAction` to omit |
|
||||
| `twocaptcha_Turnstile(apiKey, siteKey, pageURL string, cData ...string)` | Solve a Cloudflare Turnstile challenge. Optional `cData` argument |
|
||||
|
||||
### Anti-Captcha
|
||||
|
||||
Functions for the [Anti-Captcha](https://anti-captcha.com) service. This is currently the only service that supports all four captcha types end-to-end.
|
||||
|
||||
| Function | Description |
|
||||
| ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| `anticaptcha_RecaptchaV2(apiKey, siteKey, pageURL string)` | Solve a Google reCAPTCHA v2 challenge |
|
||||
| `anticaptcha_RecaptchaV3(apiKey, siteKey, pageURL, pageAction string)` | Solve a Google reCAPTCHA v3 challenge. `minScore` is hardcoded to `0.3` (Anti-Captcha rejects the request without it) |
|
||||
| `anticaptcha_HCaptcha(apiKey, siteKey, pageURL string)` | Solve an hCaptcha challenge |
|
||||
| `anticaptcha_Turnstile(apiKey, siteKey, pageURL string, cData ...string)` | Solve a Cloudflare Turnstile challenge. Optional `cData` argument |
|
||||
|
||||
### CapSolver
|
||||
|
||||
Functions for the [CapSolver](https://capsolver.com) service. Note: CapSolver no longer supports hCaptcha.
|
||||
|
||||
| Function | Description |
|
||||
| ----------------------------------------------------------------------- | ------------------------------------------------------------------------- |
|
||||
| `capsolver_RecaptchaV2(apiKey, siteKey, pageURL string)` | Solve a Google reCAPTCHA v2 challenge |
|
||||
| `capsolver_RecaptchaV3(apiKey, siteKey, pageURL, pageAction string)` | Solve a Google reCAPTCHA v3 challenge. Pass `""` for `pageAction` to omit |
|
||||
| `capsolver_Turnstile(apiKey, siteKey, pageURL string, cData ...string)` | Solve a Cloudflare Turnstile challenge. Optional `cData` argument |
|
||||
|
||||
**Examples:**
|
||||
|
||||
```yaml
|
||||
# reCAPTCHA v2 in a JSON body via 2Captcha
|
||||
method: POST
|
||||
url: https://example.com/login
|
||||
body: |
|
||||
{
|
||||
"username": "test",
|
||||
"g-recaptcha-response": "{{ twocaptcha_RecaptchaV2 "YOUR_API_KEY" "6LfD3PIb..." "https://example.com/login" }}"
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# Turnstile via Anti-Captcha with cData
|
||||
method: POST
|
||||
url: https://example.com/submit
|
||||
body: |
|
||||
{
|
||||
"cf-turnstile-response": "{{ anticaptcha_Turnstile "YOUR_API_KEY" "0x4AAAAAAA..." "https://example.com/submit" "session-cdata" }}"
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# hCaptcha via Anti-Captcha (the only service that still supports it)
|
||||
method: POST
|
||||
url: https://example.com/protected
|
||||
body: |
|
||||
{
|
||||
"h-captcha-response": "{{ anticaptcha_HCaptcha "YOUR_API_KEY" "338af34c-..." "https://example.com/protected" }}"
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
# Share a single solved token across body and headers via values
|
||||
values: 'TOKEN={{ capsolver_Turnstile "YOUR_API_KEY" "0x4AAAAAAA..." "https://example.com" }}'
|
||||
headers:
|
||||
X-Turnstile-Token: "{{ .Values.TOKEN }}"
|
||||
body: '{"token": "{{ .Values.TOKEN }}"}'
|
||||
```
|
||||
|
||||
## Fake Data Functions
|
||||
|
||||
These functions are powered by [gofakeit](https://github.com/brianvoe/gofakeit) library.
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -12,16 +13,27 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
captchaPollInterval = 5 * time.Second
|
||||
captchaPollInterval = 1 * time.Second
|
||||
captchaTimeout = 120 * time.Second
|
||||
)
|
||||
|
||||
var captchaHTTPClient = &http.Client{Timeout: captchaTimeout}
|
||||
|
||||
// solveCaptcha creates a task and polls for the result.
|
||||
// solveCaptcha creates a task on the given captcha service and polls until it is solved,
|
||||
// returning the extracted token from the solution object.
|
||||
//
|
||||
// baseURL is the service API base (e.g. "https://api.2captcha.com").
|
||||
// taskIDIsString controls whether taskId is sent back as a string or number.
|
||||
// task is the task payload the service expects (type + service-specific fields).
|
||||
// solutionKey is the field name in the solution object that holds the token.
|
||||
// taskIDIsString controls whether taskId is sent back as a string (CapSolver UUIDs)
|
||||
// or a JSON number (2Captcha, Anti-Captcha).
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaKeyEmpty
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func solveCaptcha(baseURL, apiKey string, task map[string]any, solutionKey string, taskIDIsString bool) (string, error) {
|
||||
if apiKey == "" {
|
||||
return "", types.ErrCaptchaKeyEmpty
|
||||
@@ -34,6 +46,13 @@ func solveCaptcha(baseURL, apiKey string, task map[string]any, solutionKey strin
|
||||
return captchaPollResult(baseURL, apiKey, taskID, solutionKey, taskIDIsString)
|
||||
}
|
||||
|
||||
// captchaCreateTask submits a task to the captcha service and returns the assigned taskId.
|
||||
// The taskId is normalized to a string: numeric IDs are preserved via json.RawMessage,
|
||||
// and quoted string IDs (CapSolver UUIDs) have their surrounding quotes stripped.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
func captchaCreateTask(baseURL, apiKey string, task map[string]any) (string, error) {
|
||||
body := map[string]any{
|
||||
"clientKey": apiKey,
|
||||
@@ -71,9 +90,21 @@ func captchaCreateTask(baseURL, apiKey string, task map[string]any) (string, err
|
||||
|
||||
// taskId may be a JSON number (2captcha, anti-captcha) or a quoted string (capsolver UUIDs).
|
||||
// Strip surrounding quotes if present so we always work with the underlying value.
|
||||
return strings.Trim(string(result.TaskID), `"`), nil
|
||||
taskID := strings.Trim(string(result.TaskID), `"`)
|
||||
if taskID == "" {
|
||||
return "", types.NewCaptchaAPIError("createTask", "EMPTY_TASK_ID", "service returned a successful response with no taskId")
|
||||
}
|
||||
return taskID, nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func captchaPollResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsString bool) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), captchaTimeout)
|
||||
defer cancel()
|
||||
@@ -86,18 +117,26 @@ func captchaPollResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsStri
|
||||
case <-ctx.Done():
|
||||
return "", types.NewCaptchaTimeoutError(taskID)
|
||||
case <-ticker.C:
|
||||
token, done, err := captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey, taskIDIsString)
|
||||
token, err := captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey, taskIDIsString)
|
||||
if errors.Is(err, types.ErrCaptchaProcessing) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if done {
|
||||
return token, nil
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsString bool) (string, bool, error) {
|
||||
// captchaGetTaskResult fetches a single task result from the captcha service.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaProcessing
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsString bool) (string, error) {
|
||||
var bodyMap map[string]any
|
||||
if taskIDIsString {
|
||||
bodyMap = map[string]any{"clientKey": apiKey, "taskId": taskID}
|
||||
@@ -107,7 +146,7 @@ func captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsS
|
||||
|
||||
data, err := json.Marshal(bodyMap)
|
||||
if err != nil {
|
||||
return "", false, types.NewCaptchaRequestError("getTaskResult", err)
|
||||
return "", types.NewCaptchaRequestError("getTaskResult", err)
|
||||
}
|
||||
|
||||
resp, err := captchaHTTPClient.Post(
|
||||
@@ -116,7 +155,7 @@ func captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsS
|
||||
bytes.NewReader(data),
|
||||
)
|
||||
if err != nil {
|
||||
return "", false, types.NewCaptchaRequestError("getTaskResult", err)
|
||||
return "", types.NewCaptchaRequestError("getTaskResult", err)
|
||||
}
|
||||
defer resp.Body.Close() //nolint:errcheck
|
||||
|
||||
@@ -128,33 +167,41 @@ func captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsS
|
||||
Solution map[string]any `json:"solution"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", false, types.NewCaptchaRequestError("getTaskResult", err)
|
||||
return "", types.NewCaptchaRequestError("getTaskResult", err)
|
||||
}
|
||||
|
||||
if result.ErrorID != 0 {
|
||||
return "", false, types.NewCaptchaAPIError("getTaskResult", result.ErrorCode, result.ErrorDescription)
|
||||
return "", types.NewCaptchaAPIError("getTaskResult", result.ErrorCode, result.ErrorDescription)
|
||||
}
|
||||
|
||||
if result.Status == "processing" || result.Status == "idle" {
|
||||
return "", false, nil
|
||||
return "", types.ErrCaptchaProcessing
|
||||
}
|
||||
|
||||
token, ok := result.Solution[solutionKey]
|
||||
if !ok {
|
||||
return "", false, types.NewCaptchaSolutionKeyError(solutionKey)
|
||||
return "", types.NewCaptchaSolutionKeyError(solutionKey)
|
||||
}
|
||||
tokenStr, ok := token.(string)
|
||||
if !ok {
|
||||
return "", false, types.NewCaptchaSolutionKeyError(solutionKey)
|
||||
return "", types.NewCaptchaSolutionKeyError(solutionKey)
|
||||
}
|
||||
|
||||
return tokenStr, true, nil
|
||||
return tokenStr, nil
|
||||
}
|
||||
|
||||
// ======================================== 2Captcha ========================================
|
||||
|
||||
const twoCaptchaBaseURL = "https://api.2captcha.com"
|
||||
|
||||
// twoCaptchaSolveRecaptchaV2 solves a Google reCAPTCHA v2 challenge via 2Captcha.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaKeyEmpty
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func twoCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) {
|
||||
return solveCaptcha(twoCaptchaBaseURL, apiKey, map[string]any{
|
||||
"type": "RecaptchaV2TaskProxyless",
|
||||
@@ -163,6 +210,15 @@ func twoCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string,
|
||||
}, "gRecaptchaResponse", false)
|
||||
}
|
||||
|
||||
// twoCaptchaSolveRecaptchaV3 solves a Google reCAPTCHA v3 challenge via 2Captcha.
|
||||
// pageAction may be empty.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaKeyEmpty
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func twoCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) {
|
||||
task := map[string]any{
|
||||
"type": "RecaptchaV3TaskProxyless",
|
||||
@@ -175,6 +231,15 @@ func twoCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction strin
|
||||
return solveCaptcha(twoCaptchaBaseURL, apiKey, task, "gRecaptchaResponse", false)
|
||||
}
|
||||
|
||||
// twoCaptchaSolveTurnstile solves a Cloudflare Turnstile challenge via 2Captcha.
|
||||
// cData may be empty.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaKeyEmpty
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func twoCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) {
|
||||
task := map[string]any{
|
||||
"type": "TurnstileTaskProxyless",
|
||||
@@ -191,6 +256,14 @@ func twoCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (str
|
||||
|
||||
const antiCaptchaBaseURL = "https://api.anti-captcha.com"
|
||||
|
||||
// antiCaptchaSolveRecaptchaV2 solves a Google reCAPTCHA v2 challenge via Anti-Captcha.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaKeyEmpty
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func antiCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) {
|
||||
return solveCaptcha(antiCaptchaBaseURL, apiKey, map[string]any{
|
||||
"type": "RecaptchaV2TaskProxyless",
|
||||
@@ -199,8 +272,17 @@ func antiCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string,
|
||||
}, "gRecaptchaResponse", false)
|
||||
}
|
||||
|
||||
// antiCaptchaSolveRecaptchaV3 solves a Google reCAPTCHA v3 challenge via Anti-Captcha.
|
||||
// pageAction may be empty. minScore is hardcoded to 0.3 (the loosest threshold) because
|
||||
// Anti-Captcha rejects the request without it.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaKeyEmpty
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func antiCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) {
|
||||
// Anti-Captcha requires minScore for reCAPTCHA v3. 0.3 is the loosest threshold.
|
||||
task := map[string]any{
|
||||
"type": "RecaptchaV3TaskProxyless",
|
||||
"websiteURL": websiteURL,
|
||||
@@ -213,8 +295,16 @@ func antiCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction stri
|
||||
return solveCaptcha(antiCaptchaBaseURL, apiKey, task, "gRecaptchaResponse", false)
|
||||
}
|
||||
|
||||
// antiCaptchaSolveHCaptcha solves an hCaptcha challenge via Anti-Captcha.
|
||||
// Anti-Captcha returns hCaptcha tokens under "gRecaptchaResponse" (not "token").
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaKeyEmpty
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func antiCaptchaSolveHCaptcha(apiKey, websiteURL, websiteKey string) (string, error) {
|
||||
// Anti-Captcha returns hCaptcha tokens under "gRecaptchaResponse" (not "token").
|
||||
return solveCaptcha(antiCaptchaBaseURL, apiKey, map[string]any{
|
||||
"type": "HCaptchaTaskProxyless",
|
||||
"websiteURL": websiteURL,
|
||||
@@ -222,6 +312,15 @@ func antiCaptchaSolveHCaptcha(apiKey, websiteURL, websiteKey string) (string, er
|
||||
}, "gRecaptchaResponse", false)
|
||||
}
|
||||
|
||||
// antiCaptchaSolveTurnstile solves a Cloudflare Turnstile challenge via Anti-Captcha.
|
||||
// cData may be empty.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaKeyEmpty
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func antiCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) {
|
||||
task := map[string]any{
|
||||
"type": "TurnstileTaskProxyless",
|
||||
@@ -238,6 +337,14 @@ func antiCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (st
|
||||
|
||||
const capSolverBaseURL = "https://api.capsolver.com"
|
||||
|
||||
// capSolverSolveRecaptchaV2 solves a Google reCAPTCHA v2 challenge via CapSolver.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaKeyEmpty
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func capSolverSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) {
|
||||
return solveCaptcha(capSolverBaseURL, apiKey, map[string]any{
|
||||
"type": "ReCaptchaV2TaskProxyLess",
|
||||
@@ -246,6 +353,15 @@ func capSolverSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, e
|
||||
}, "gRecaptchaResponse", true)
|
||||
}
|
||||
|
||||
// capSolverSolveRecaptchaV3 solves a Google reCAPTCHA v3 challenge via CapSolver.
|
||||
// pageAction may be empty.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaKeyEmpty
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func capSolverSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) {
|
||||
task := map[string]any{
|
||||
"type": "ReCaptchaV3TaskProxyLess",
|
||||
@@ -258,6 +374,15 @@ func capSolverSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string
|
||||
return solveCaptcha(capSolverBaseURL, apiKey, task, "gRecaptchaResponse", true)
|
||||
}
|
||||
|
||||
// capSolverSolveTurnstile solves a Cloudflare Turnstile challenge via CapSolver.
|
||||
// cData may be empty. CapSolver nests cData under a "metadata" object.
|
||||
//
|
||||
// It can return the following errors:
|
||||
// - types.ErrCaptchaKeyEmpty
|
||||
// - types.CaptchaRequestError
|
||||
// - types.CaptchaAPIError
|
||||
// - types.CaptchaTimeoutError
|
||||
// - types.CaptchaSolutionKeyError
|
||||
func capSolverSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) {
|
||||
task := map[string]any{
|
||||
"type": "AntiTurnstileTaskProxyLess",
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"math/rand/v2"
|
||||
"mime/multipart"
|
||||
"strings"
|
||||
@@ -85,6 +86,38 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
|
||||
"slice_Uint": func(values ...uint) []uint { return values },
|
||||
"slice_Join": strings.Join,
|
||||
|
||||
// JSON
|
||||
// json_Encode marshals any value to a JSON string.
|
||||
// Usage: {{ json_Encode (dict_Str "key" "value") }}
|
||||
"json_Encode": func(v any) (string, error) {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", types.NewJSONEncodeError(err)
|
||||
}
|
||||
return string(data), nil
|
||||
},
|
||||
// json_Object builds a JSON object from interleaved key-value pairs and returns it
|
||||
// as a JSON string. Keys must be strings; values may be any JSON-encodable type.
|
||||
// Usage: {{ json_Object "name" "Alice" "age" 30 }}
|
||||
"json_Object": func(pairs ...any) (string, error) {
|
||||
if len(pairs)%2 != 0 {
|
||||
return "", types.ErrJSONObjectOddArgs
|
||||
}
|
||||
obj := make(map[string]any, len(pairs)/2)
|
||||
for i := 0; i < len(pairs); i += 2 {
|
||||
key, ok := pairs[i].(string)
|
||||
if !ok {
|
||||
return "", types.NewJSONObjectKeyError(i, pairs[i])
|
||||
}
|
||||
obj[key] = pairs[i+1]
|
||||
}
|
||||
data, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return "", types.NewJSONEncodeError(err)
|
||||
}
|
||||
return string(data), nil
|
||||
},
|
||||
|
||||
// Time
|
||||
"time_NowUnix": func() int64 { return time.Now().Unix() },
|
||||
"time_NowUnixMilli": func() int64 { return time.Now().UnixMilli() },
|
||||
|
||||
@@ -208,8 +208,41 @@ func (e URLParseError) Unwrap() error {
|
||||
var (
|
||||
ErrFileCacheNotInitialized = errors.New("file cache is not initialized")
|
||||
ErrFormDataOddArgs = errors.New("body_FormData requires an even number of arguments (key-value pairs)")
|
||||
ErrJSONObjectOddArgs = errors.New("json_Object requires an even number of arguments (key-value pairs)")
|
||||
)
|
||||
|
||||
type JSONObjectKeyError struct {
|
||||
Index int
|
||||
Value any
|
||||
}
|
||||
|
||||
func NewJSONObjectKeyError(index int, value any) JSONObjectKeyError {
|
||||
return JSONObjectKeyError{Index: index, Value: value}
|
||||
}
|
||||
|
||||
func (e JSONObjectKeyError) Error() string {
|
||||
return fmt.Sprintf("json_Object key at index %d must be a string, got %T", e.Index, e.Value)
|
||||
}
|
||||
|
||||
type JSONEncodeError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func NewJSONEncodeError(err error) JSONEncodeError {
|
||||
if err == nil {
|
||||
err = errNoError
|
||||
}
|
||||
return JSONEncodeError{Err: err}
|
||||
}
|
||||
|
||||
func (e JSONEncodeError) Error() string {
|
||||
return "json_Encode failed: " + e.Err.Error()
|
||||
}
|
||||
|
||||
func (e JSONEncodeError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
type TemplateParseError struct {
|
||||
Err error
|
||||
}
|
||||
@@ -445,7 +478,13 @@ func (e ScriptUnknownEngineError) Error() string {
|
||||
|
||||
// ======================================== Captcha ========================================
|
||||
|
||||
var ErrCaptchaKeyEmpty = errors.New("captcha API key cannot be empty")
|
||||
var (
|
||||
ErrCaptchaKeyEmpty = errors.New("captcha API key cannot be empty")
|
||||
// ErrCaptchaProcessing is an internal sentinel returned by the captcha solver polling
|
||||
// code to signal that a task is not yet solved and polling should continue.
|
||||
// It should never be surfaced to callers outside of the captcha poll loop.
|
||||
ErrCaptchaProcessing = errors.New("captcha task still processing")
|
||||
)
|
||||
|
||||
type CaptchaAPIError struct {
|
||||
Endpoint string
|
||||
|
||||
Reference in New Issue
Block a user