mirror of
https://github.com/aykhans/sarin.git
synced 2026-04-15 04:29:35 +00:00
Compare commits
2 Commits
feat/captc
...
0e0ef72778
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e0ef72778 | |||
|
|
8d10198f02 |
@@ -26,11 +26,10 @@ Sarin is designed for efficient HTTP load testing with minimal resource consumpt
|
|||||||
| ---------------------------------------------------------- | ------------------------------- |
|
| ---------------------------------------------------------- | ------------------------------- |
|
||||||
| High-performance with low memory footprint | Detailed response body analysis |
|
| High-performance with low memory footprint | Detailed response body analysis |
|
||||||
| Long-running duration/count based tests | Extensive response statistics |
|
| Long-running duration/count based tests | Extensive response statistics |
|
||||||
| Dynamic requests via 340+ template functions | Web UI or complex TUI |
|
| Dynamic requests via 320+ template functions | Web UI or complex TUI |
|
||||||
| Request scripting with Lua and JavaScript | Distributed load testing |
|
| Request scripting with Lua and JavaScript | Distributed load testing |
|
||||||
| Multiple proxy protocols<br>(HTTP, HTTPS, SOCKS5, SOCKS5H) | HTTP/2, HTTP/3, WebSocket, gRPC |
|
| Multiple proxy protocols<br>(HTTP, HTTPS, SOCKS5, SOCKS5H) | HTTP/2, HTTP/3, WebSocket, gRPC |
|
||||||
| Captcha solving<br>(2Captcha, Anti-Captcha, CapSolver) | Plugins / extensions ecosystem |
|
| Flexible config (CLI, ENV, YAML) | Plugins / extensions ecosystem |
|
||||||
| Flexible config (CLI, ENV, YAML) | |
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ This guide provides practical examples for common Sarin use cases.
|
|||||||
- [Request-Based vs Duration-Based Tests](#request-based-vs-duration-based-tests)
|
- [Request-Based vs Duration-Based Tests](#request-based-vs-duration-based-tests)
|
||||||
- [Headers, Cookies, and Parameters](#headers-cookies-and-parameters)
|
- [Headers, Cookies, and Parameters](#headers-cookies-and-parameters)
|
||||||
- [Dynamic Requests with Templating](#dynamic-requests-with-templating)
|
- [Dynamic Requests with Templating](#dynamic-requests-with-templating)
|
||||||
- [Solving Captchas](#solving-captchas)
|
|
||||||
- [Request Bodies](#request-bodies)
|
- [Request Bodies](#request-bodies)
|
||||||
- [File Uploads](#file-uploads)
|
- [File Uploads](#file-uploads)
|
||||||
- [Using Proxies](#using-proxies)
|
- [Using Proxies](#using-proxies)
|
||||||
@@ -372,29 +371,7 @@ body: '{"ip": "{{ fakeit_IPv4Address }}", "timestamp": "{{ fakeit_Date }}", "act
|
|||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
> For the complete list of 340+ template functions, see the **[Templating Guide](templating.md)**.
|
> For the complete list of 320+ 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
|
## Request Bodies
|
||||||
|
|
||||||
|
|||||||
@@ -4,23 +4,16 @@ 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:** 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
|
## Table of Contents
|
||||||
|
|
||||||
- [Using Values](#using-values)
|
- [Using Values](#using-values)
|
||||||
- [General Functions](#general-functions)
|
- [General Functions](#general-functions)
|
||||||
- [String Functions](#string-functions)
|
- [String Functions](#string-functions)
|
||||||
- [Collection Functions](#collection-functions)
|
- [Collection Functions](#collection-functions)
|
||||||
- [JSON Functions](#json-functions)
|
|
||||||
- [Time Functions](#time-functions)
|
- [Time Functions](#time-functions)
|
||||||
- [Crypto Functions](#crypto-functions)
|
- [Crypto Functions](#crypto-functions)
|
||||||
- [Body Functions](#body-functions)
|
- [Body Functions](#body-functions)
|
||||||
- [File Functions](#file-functions)
|
- [File Functions](#file-functions)
|
||||||
- [Captcha Functions](#captcha-functions)
|
|
||||||
- [2Captcha](#2captcha)
|
|
||||||
- [Anti-Captcha](#anti-captcha)
|
|
||||||
- [CapSolver](#capsolver)
|
|
||||||
- [Fake Data Functions](#fake-data-functions)
|
- [Fake Data Functions](#fake-data-functions)
|
||||||
- [File](#file)
|
- [File](#file)
|
||||||
- [ID](#id)
|
- [ID](#id)
|
||||||
@@ -118,33 +111,6 @@ sarin -U http://example.com/users \
|
|||||||
| `slice_Int(values ...int)` | Create int slice | `{{ slice_Int 1 2 3 }}` |
|
| `slice_Int(values ...int)` | Create int slice | `{{ slice_Int 1 2 3 }}` |
|
||||||
| `slice_Uint(values ...uint)` | Create uint slice | `{{ slice_Uint 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
|
### Time Functions
|
||||||
|
|
||||||
| Function | Description | Example |
|
| Function | Description | Example |
|
||||||
@@ -230,95 +196,6 @@ values: "FILE_DATA={{ file_Base64 \"/path/to/file.bin\" }}"
|
|||||||
body: '{"data": "{{ .Values.FILE_DATA }}"}'
|
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
|
## Fake Data Functions
|
||||||
|
|
||||||
These functions are powered by [gofakeit](https://github.com/brianvoe/gofakeit) library.
|
These functions are powered by [gofakeit](https://github.com/brianvoe/gofakeit) library.
|
||||||
|
|||||||
8
go.mod
8
go.mod
@@ -15,7 +15,7 @@ require (
|
|||||||
github.com/yuin/gopher-lua v1.1.2
|
github.com/yuin/gopher-lua v1.1.2
|
||||||
go.aykhans.me/utils v1.0.7
|
go.aykhans.me/utils v1.0.7
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.4
|
go.yaml.in/yaml/v4 v4.0.0-rc.4
|
||||||
golang.org/x/net v0.52.0
|
golang.org/x/net v0.53.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -52,7 +52,7 @@ require (
|
|||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
github.com/yuin/goldmark v1.8.2 // indirect
|
github.com/yuin/goldmark v1.8.2 // indirect
|
||||||
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
github.com/yuin/goldmark-emoji v1.0.6 // indirect
|
||||||
golang.org/x/sys v0.42.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
golang.org/x/term v0.41.0 // indirect
|
golang.org/x/term v0.42.0 // indirect
|
||||||
golang.org/x/text v0.35.0 // indirect
|
golang.org/x/text v0.36.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
16
go.sum
16
go.sum
@@ -109,16 +109,16 @@ go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
|
|||||||
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
go.yaml.in/yaml/v4 v4.0.0-rc.4/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
||||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
|||||||
@@ -1,415 +0,0 @@
|
|||||||
package sarin
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"go.aykhans.me/sarin/internal/types"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
captchaPollInterval = 1 * time.Second
|
|
||||||
captchaPollTimeout = 120 * time.Second
|
|
||||||
)
|
|
||||||
|
|
||||||
var captchaHTTPClient = &http.Client{Timeout: 5 * time.Second}
|
|
||||||
|
|
||||||
// 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").
|
|
||||||
// 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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func solveCaptcha(baseURL, apiKey string, task map[string]any, solutionKey string, taskIDIsString bool) (string, error) {
|
|
||||||
if apiKey == "" {
|
|
||||||
return "", types.ErrCaptchaKeyEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
taskID, err := captchaCreateTask(baseURL, apiKey, task)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
func captchaCreateTask(baseURL, apiKey string, task map[string]any) (string, error) {
|
|
||||||
body := map[string]any{
|
|
||||||
"clientKey": apiKey,
|
|
||||||
"task": task,
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(body)
|
|
||||||
if err != nil {
|
|
||||||
return "", types.NewCaptchaDecodeError("createTask", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := captchaHTTPClient.Post(
|
|
||||||
baseURL+"/createTask",
|
|
||||||
"application/json",
|
|
||||||
bytes.NewReader(data),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return "", types.NewCaptchaRequestError("createTask", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close() //nolint:errcheck
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
ErrorID int `json:"errorId"`
|
|
||||||
ErrorCode string `json:"errorCode"`
|
|
||||||
ErrorDescription string `json:"errorDescription"`
|
|
||||||
TaskID json.RawMessage `json:"taskId"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
||||||
return "", types.NewCaptchaDecodeError("createTask", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.ErrorID != 0 {
|
|
||||||
return "", types.NewCaptchaAPIError("createTask", result.ErrorCode, result.ErrorDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
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 captchaPollTimeout is hit.
|
|
||||||
//
|
|
||||||
// It can return the following errors:
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func captchaPollResult(baseURL, apiKey, taskID, solutionKey string, taskIDIsString bool) (string, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), captchaPollTimeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
ticker := time.NewTicker(captchaPollInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return "", types.NewCaptchaPollTimeoutError(taskID)
|
|
||||||
case <-ticker.C:
|
|
||||||
token, err := captchaGetTaskResult(baseURL, apiKey, taskID, solutionKey, taskIDIsString)
|
|
||||||
if errors.Is(err, types.ErrCaptchaProcessing) {
|
|
||||||
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 {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return token, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// captchaGetTaskResult fetches a single task result from the captcha service.
|
|
||||||
//
|
|
||||||
// It can return the following errors:
|
|
||||||
// - types.ErrCaptchaProcessing
|
|
||||||
// - types.CaptchaRequestError
|
|
||||||
// - types.CaptchaDecodeError
|
|
||||||
// - 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}
|
|
||||||
} else {
|
|
||||||
bodyMap = map[string]any{"clientKey": apiKey, "taskId": json.Number(taskID)}
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := json.Marshal(bodyMap)
|
|
||||||
if err != nil {
|
|
||||||
return "", types.NewCaptchaDecodeError("getTaskResult", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := captchaHTTPClient.Post(
|
|
||||||
baseURL+"/getTaskResult",
|
|
||||||
"application/json",
|
|
||||||
bytes.NewReader(data),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return "", types.NewCaptchaRequestError("getTaskResult", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close() //nolint:errcheck
|
|
||||||
|
|
||||||
var result struct {
|
|
||||||
ErrorID int `json:"errorId"`
|
|
||||||
ErrorCode string `json:"errorCode"`
|
|
||||||
ErrorDescription string `json:"errorDescription"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Solution map[string]any `json:"solution"`
|
|
||||||
}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
||||||
return "", types.NewCaptchaDecodeError("getTaskResult", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.ErrorID != 0 {
|
|
||||||
return "", types.NewCaptchaAPIError("getTaskResult", result.ErrorCode, result.ErrorDescription)
|
|
||||||
}
|
|
||||||
|
|
||||||
if result.Status == "processing" || result.Status == "idle" {
|
|
||||||
return "", types.ErrCaptchaProcessing
|
|
||||||
}
|
|
||||||
|
|
||||||
token, ok := result.Solution[solutionKey]
|
|
||||||
if !ok {
|
|
||||||
return "", types.NewCaptchaSolutionKeyError(solutionKey)
|
|
||||||
}
|
|
||||||
tokenStr, ok := token.(string)
|
|
||||||
if !ok {
|
|
||||||
return "", types.NewCaptchaSolutionKeyError(solutionKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func twoCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) {
|
|
||||||
return solveCaptcha(twoCaptchaBaseURL, apiKey, map[string]any{
|
|
||||||
"type": "RecaptchaV2TaskProxyless",
|
|
||||||
"websiteURL": websiteURL,
|
|
||||||
"websiteKey": websiteKey,
|
|
||||||
}, "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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func twoCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) {
|
|
||||||
task := map[string]any{
|
|
||||||
"type": "RecaptchaV3TaskProxyless",
|
|
||||||
"websiteURL": websiteURL,
|
|
||||||
"websiteKey": websiteKey,
|
|
||||||
}
|
|
||||||
if pageAction != "" {
|
|
||||||
task["pageAction"] = pageAction
|
|
||||||
}
|
|
||||||
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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func twoCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) {
|
|
||||||
task := map[string]any{
|
|
||||||
"type": "TurnstileTaskProxyless",
|
|
||||||
"websiteURL": websiteURL,
|
|
||||||
"websiteKey": websiteKey,
|
|
||||||
}
|
|
||||||
if cData != "" {
|
|
||||||
task["data"] = cData
|
|
||||||
}
|
|
||||||
return solveCaptcha(twoCaptchaBaseURL, apiKey, task, "token", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================================== Anti-Captcha ========================================
|
|
||||||
|
|
||||||
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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func antiCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) {
|
|
||||||
return solveCaptcha(antiCaptchaBaseURL, apiKey, map[string]any{
|
|
||||||
"type": "RecaptchaV2TaskProxyless",
|
|
||||||
"websiteURL": websiteURL,
|
|
||||||
"websiteKey": websiteKey,
|
|
||||||
}, "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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func antiCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) {
|
|
||||||
task := map[string]any{
|
|
||||||
"type": "RecaptchaV3TaskProxyless",
|
|
||||||
"websiteURL": websiteURL,
|
|
||||||
"websiteKey": websiteKey,
|
|
||||||
"minScore": 0.3,
|
|
||||||
}
|
|
||||||
if pageAction != "" {
|
|
||||||
task["pageAction"] = pageAction
|
|
||||||
}
|
|
||||||
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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func antiCaptchaSolveHCaptcha(apiKey, websiteURL, websiteKey string) (string, error) {
|
|
||||||
return solveCaptcha(antiCaptchaBaseURL, apiKey, map[string]any{
|
|
||||||
"type": "HCaptchaTaskProxyless",
|
|
||||||
"websiteURL": websiteURL,
|
|
||||||
"websiteKey": websiteKey,
|
|
||||||
}, "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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func antiCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) {
|
|
||||||
task := map[string]any{
|
|
||||||
"type": "TurnstileTaskProxyless",
|
|
||||||
"websiteURL": websiteURL,
|
|
||||||
"websiteKey": websiteKey,
|
|
||||||
}
|
|
||||||
if cData != "" {
|
|
||||||
task["cData"] = cData
|
|
||||||
}
|
|
||||||
return solveCaptcha(antiCaptchaBaseURL, apiKey, task, "token", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================================== CapSolver ========================================
|
|
||||||
|
|
||||||
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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func capSolverSolveRecaptchaV2(apiKey, websiteURL, websiteKey string) (string, error) {
|
|
||||||
return solveCaptcha(capSolverBaseURL, apiKey, map[string]any{
|
|
||||||
"type": "ReCaptchaV2TaskProxyLess",
|
|
||||||
"websiteURL": websiteURL,
|
|
||||||
"websiteKey": websiteKey,
|
|
||||||
}, "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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func capSolverSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction string) (string, error) {
|
|
||||||
task := map[string]any{
|
|
||||||
"type": "ReCaptchaV3TaskProxyLess",
|
|
||||||
"websiteURL": websiteURL,
|
|
||||||
"websiteKey": websiteKey,
|
|
||||||
}
|
|
||||||
if pageAction != "" {
|
|
||||||
task["pageAction"] = pageAction
|
|
||||||
}
|
|
||||||
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.CaptchaDecodeError
|
|
||||||
// - types.CaptchaAPIError
|
|
||||||
// - types.CaptchaPollTimeoutError
|
|
||||||
// - types.CaptchaSolutionKeyError
|
|
||||||
func capSolverSolveTurnstile(apiKey, websiteURL, websiteKey, cData string) (string, error) {
|
|
||||||
task := map[string]any{
|
|
||||||
"type": "AntiTurnstileTaskProxyLess",
|
|
||||||
"websiteURL": websiteURL,
|
|
||||||
"websiteKey": websiteKey,
|
|
||||||
}
|
|
||||||
if cData != "" {
|
|
||||||
task["metadata"] = map[string]any{"cdata": cData}
|
|
||||||
}
|
|
||||||
return solveCaptcha(capSolverBaseURL, apiKey, task, "token", true)
|
|
||||||
}
|
|
||||||
@@ -12,10 +12,3 @@ func NewDefaultRandSource() rand.Source {
|
|||||||
uint64(now>>32),
|
uint64(now>>32),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func firstOrEmpty(values []string) string {
|
|
||||||
if len(values) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return values[0]
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -86,38 +85,6 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
|
|||||||
"slice_Uint": func(values ...uint) []uint { return values },
|
"slice_Uint": func(values ...uint) []uint { return values },
|
||||||
"slice_Join": strings.Join,
|
"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
|
||||||
"time_NowUnix": func() int64 { return time.Now().Unix() },
|
"time_NowUnix": func() int64 { return time.Now().Unix() },
|
||||||
"time_NowUnixMilli": func() int64 { return time.Now().UnixMilli() },
|
"time_NowUnixMilli": func() int64 { return time.Now().UnixMilli() },
|
||||||
@@ -607,6 +574,7 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
|
|||||||
"fakeit_ErrorHTTP": func() string { return fakeit.ErrorHTTP().Error() },
|
"fakeit_ErrorHTTP": func() string { return fakeit.ErrorHTTP().Error() },
|
||||||
"fakeit_ErrorHTTPClient": func() string { return fakeit.ErrorHTTPClient().Error() },
|
"fakeit_ErrorHTTPClient": func() string { return fakeit.ErrorHTTPClient().Error() },
|
||||||
"fakeit_ErrorHTTPServer": func() string { return fakeit.ErrorHTTPServer().Error() },
|
"fakeit_ErrorHTTPServer": func() string { return fakeit.ErrorHTTPServer().Error() },
|
||||||
|
// "fakeit_ErrorInput": func() string { return fakeit.ErrorInput().Error() },
|
||||||
"fakeit_ErrorRuntime": func() string { return fakeit.ErrorRuntime().Error() },
|
"fakeit_ErrorRuntime": func() string { return fakeit.ErrorRuntime().Error() },
|
||||||
|
|
||||||
// Fakeit / School
|
// Fakeit / School
|
||||||
@@ -617,55 +585,6 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
|
|||||||
"fakeit_SongName": fakeit.SongName,
|
"fakeit_SongName": fakeit.SongName,
|
||||||
"fakeit_SongArtist": fakeit.SongArtist,
|
"fakeit_SongArtist": fakeit.SongArtist,
|
||||||
"fakeit_SongGenre": fakeit.SongGenre,
|
"fakeit_SongGenre": fakeit.SongGenre,
|
||||||
|
|
||||||
// Captcha / 2Captcha
|
|
||||||
// Usage: {{ twocaptcha_RecaptchaV2 "API_KEY" "SITE_KEY" "https://example.com" }}
|
|
||||||
"twocaptcha_RecaptchaV2": func(apiKey, websiteKey, websiteURL string) (string, error) {
|
|
||||||
return twoCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey)
|
|
||||||
},
|
|
||||||
// Usage: {{ twocaptcha_RecaptchaV3 "API_KEY" "SITE_KEY" "https://example.com" "action" }}
|
|
||||||
"twocaptcha_RecaptchaV3": func(apiKey, websiteKey, websiteURL, pageAction string) (string, error) {
|
|
||||||
return twoCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction)
|
|
||||||
},
|
|
||||||
// Usage: {{ twocaptcha_Turnstile "API_KEY" "SITE_KEY" "https://example.com" }}
|
|
||||||
// {{ twocaptcha_Turnstile "API_KEY" "SITE_KEY" "https://example.com" "cdata" }}
|
|
||||||
"twocaptcha_Turnstile": func(apiKey, websiteKey, websiteURL string, cData ...string) (string, error) {
|
|
||||||
return twoCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, firstOrEmpty(cData))
|
|
||||||
},
|
|
||||||
|
|
||||||
// Captcha / Anti-Captcha
|
|
||||||
// Usage: {{ anticaptcha_RecaptchaV2 "API_KEY" "SITE_KEY" "https://example.com" }}
|
|
||||||
"anticaptcha_RecaptchaV2": func(apiKey, websiteKey, websiteURL string) (string, error) {
|
|
||||||
return antiCaptchaSolveRecaptchaV2(apiKey, websiteURL, websiteKey)
|
|
||||||
},
|
|
||||||
// Usage: {{ anticaptcha_RecaptchaV3 "API_KEY" "SITE_KEY" "https://example.com" "action" }}
|
|
||||||
"anticaptcha_RecaptchaV3": func(apiKey, websiteKey, websiteURL, pageAction string) (string, error) {
|
|
||||||
return antiCaptchaSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction)
|
|
||||||
},
|
|
||||||
// Usage: {{ anticaptcha_HCaptcha "API_KEY" "SITE_KEY" "https://example.com" }}
|
|
||||||
"anticaptcha_HCaptcha": func(apiKey, websiteKey, websiteURL string) (string, error) {
|
|
||||||
return antiCaptchaSolveHCaptcha(apiKey, websiteURL, websiteKey)
|
|
||||||
},
|
|
||||||
// Usage: {{ anticaptcha_Turnstile "API_KEY" "SITE_KEY" "https://example.com" }}
|
|
||||||
// {{ anticaptcha_Turnstile "API_KEY" "SITE_KEY" "https://example.com" "cdata" }}
|
|
||||||
"anticaptcha_Turnstile": func(apiKey, websiteKey, websiteURL string, cData ...string) (string, error) {
|
|
||||||
return antiCaptchaSolveTurnstile(apiKey, websiteURL, websiteKey, firstOrEmpty(cData))
|
|
||||||
},
|
|
||||||
|
|
||||||
// Captcha / CapSolver
|
|
||||||
// Usage: {{ capsolver_RecaptchaV2 "API_KEY" "SITE_KEY" "https://example.com" }}
|
|
||||||
"capsolver_RecaptchaV2": func(apiKey, websiteKey, websiteURL string) (string, error) {
|
|
||||||
return capSolverSolveRecaptchaV2(apiKey, websiteURL, websiteKey)
|
|
||||||
},
|
|
||||||
// Usage: {{ capsolver_RecaptchaV3 "API_KEY" "SITE_KEY" "https://example.com" "action" }}
|
|
||||||
"capsolver_RecaptchaV3": func(apiKey, websiteKey, websiteURL, pageAction string) (string, error) {
|
|
||||||
return capSolverSolveRecaptchaV3(apiKey, websiteURL, websiteKey, pageAction)
|
|
||||||
},
|
|
||||||
// Usage: {{ capsolver_Turnstile "API_KEY" "SITE_KEY" "https://example.com" }}
|
|
||||||
// {{ capsolver_Turnstile "API_KEY" "SITE_KEY" "https://example.com" "cdata" }}
|
|
||||||
"capsolver_Turnstile": func(apiKey, websiteKey, websiteURL string, cData ...string) (string, error) {
|
|
||||||
return capSolverSolveTurnstile(apiKey, websiteURL, websiteKey, firstOrEmpty(cData))
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -208,41 +208,8 @@ func (e URLParseError) Unwrap() error {
|
|||||||
var (
|
var (
|
||||||
ErrFileCacheNotInitialized = errors.New("file cache is not initialized")
|
ErrFileCacheNotInitialized = errors.New("file cache is not initialized")
|
||||||
ErrFormDataOddArgs = errors.New("body_FormData requires an even number of arguments (key-value pairs)")
|
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 {
|
type TemplateParseError struct {
|
||||||
Err error
|
Err error
|
||||||
}
|
}
|
||||||
@@ -475,91 +442,3 @@ func NewScriptUnknownEngineError(engineType string) ScriptUnknownEngineError {
|
|||||||
func (e ScriptUnknownEngineError) Error() string {
|
func (e ScriptUnknownEngineError) Error() string {
|
||||||
return "unknown engine type: " + e.EngineType
|
return "unknown engine type: " + e.EngineType
|
||||||
}
|
}
|
||||||
|
|
||||||
// ======================================== Captcha ========================================
|
|
||||||
|
|
||||||
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
|
|
||||||
Code string
|
|
||||||
Description string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCaptchaAPIError(endpoint, code, description string) CaptchaAPIError {
|
|
||||||
return CaptchaAPIError{Endpoint: endpoint, Code: code, Description: description}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e CaptchaAPIError) Error() string {
|
|
||||||
return fmt.Sprintf("captcha %s error: %s (%s)", e.Endpoint, e.Code, e.Description)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CaptchaRequestError struct {
|
|
||||||
Endpoint string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCaptchaRequestError(endpoint string, err error) CaptchaRequestError {
|
|
||||||
if err == nil {
|
|
||||||
err = errNoError
|
|
||||||
}
|
|
||||||
return CaptchaRequestError{Endpoint: endpoint, Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e CaptchaRequestError) Error() string {
|
|
||||||
return fmt.Sprintf("captcha %s request failed: %v", e.Endpoint, e.Err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e CaptchaRequestError) Unwrap() error {
|
|
||||||
return e.Err
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCaptchaPollTimeoutError(taskID string) CaptchaPollTimeoutError {
|
|
||||||
return CaptchaPollTimeoutError{TaskID: taskID}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e CaptchaPollTimeoutError) Error() string {
|
|
||||||
return fmt.Sprintf("captcha solving timed out (taskId: %s)", e.TaskID)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CaptchaSolutionKeyError struct {
|
|
||||||
Key string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCaptchaSolutionKeyError(key string) CaptchaSolutionKeyError {
|
|
||||||
return CaptchaSolutionKeyError{Key: key}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e CaptchaSolutionKeyError) Error() string {
|
|
||||||
return fmt.Sprintf("captcha solution missing expected key %q", e.Key)
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user