mirror of
https://github.com/aykhans/sarin.git
synced 2026-04-15 12:29:36 +00:00
Compare commits
28 Commits
ae054bb3d6
...
feat/captc
| Author | SHA1 | Date | |
|---|---|---|---|
| 8577c771e4 | |||
| c839b71c9e | |||
| cea692cf1b | |||
| 88f5171132 | |||
| 16b0081d3e | |||
| 1bd58a02b7 | |||
| 006029aad1 | |||
| cf3c8f4cde | |||
|
|
65ef05f960 | ||
| 14280f4e43 | |||
|
|
c95b06b1ad | ||
| 88d6a0132e | |||
| e33c549f6d | |||
| 4a4feb4570 | |||
| 304fb160f8 | |||
| 44c35e6681 | |||
| 9215fd8767 | |||
|
|
8879a59159 | ||
| 705f6263fe | |||
| 9c5b998cda | |||
| 026d05f1bf | |||
| 844f139a10 | |||
|
|
d767ac6f37 | ||
| c299fda79d | |||
| 1f06b43b06 | |||
| e031c8e7a5 | |||
|
|
de24f9d4a4 | ||
| d197e90103 |
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
@@ -1,2 +1 @@
|
|||||||
buy_me_a_coffee: aykhan
|
buy_me_a_coffee: aykhan
|
||||||
custom: https://commerce.coinbase.com/checkout/0f33d2fb-54a6-44f5-8783-006ebf70d1a0
|
|
||||||
|
|||||||
4
.github/workflows/lint.yaml
vendored
4
.github/workflows/lint.yaml
vendored
@@ -16,7 +16,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
- uses: actions/setup-go@v6
|
- uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.0
|
go-version: 1.26.1
|
||||||
- name: go fix
|
- name: go fix
|
||||||
run: |
|
run: |
|
||||||
go fix ./...
|
go fix ./...
|
||||||
@@ -24,4 +24,4 @@ jobs:
|
|||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v9
|
uses: golangci/golangci-lint-action@v9
|
||||||
with:
|
with:
|
||||||
version: v2.9.0
|
version: v2.11.4
|
||||||
|
|||||||
2
.github/workflows/release.yaml
vendored
2
.github/workflows/release.yaml
vendored
@@ -35,7 +35,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "VERSION=$(git describe --tags --always)" >> $GITHUB_ENV
|
echo "VERSION=$(git describe --tags --always)" >> $GITHUB_ENV
|
||||||
echo "GIT_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
echo "GIT_COMMIT=$(git rev-parse HEAD)" >> $GITHUB_ENV
|
||||||
echo "GO_VERSION=1.26.0" >> $GITHUB_ENV
|
echo "GO_VERSION=1.26.1" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
if: github.event_name == 'release' || inputs.build_binaries
|
if: github.event_name == 'release' || inputs.build_binaries
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
ARG GO_VERSION=1.26.0
|
ARG GO_VERSION=1.26.1
|
||||||
|
|
||||||
FROM docker.io/library/golang:${GO_VERSION}-alpine AS builder
|
FROM docker.io/library/golang:${GO_VERSION}-alpine AS builder
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
| 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 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 |
|
| 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 |
|
||||||
| Flexible config (CLI, ENV, YAML) | Plugins / extensions ecosystem |
|
| Captcha solving<br>(2Captcha, Anti-Captcha, CapSolver) | Plugins / extensions ecosystem |
|
||||||
|
| Flexible config (CLI, ENV, YAML) | |
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ version: "3"
|
|||||||
|
|
||||||
vars:
|
vars:
|
||||||
BIN_DIR: ./bin
|
BIN_DIR: ./bin
|
||||||
GOLANGCI_LINT_VERSION: v2.9.0
|
GOLANGCI_LINT_VERSION: v2.11.4
|
||||||
GOLANGCI: "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}"
|
GOLANGCI: "{{.BIN_DIR}}/golangci-lint-{{.GOLANGCI_LINT_VERSION}}"
|
||||||
|
|
||||||
tasks:
|
tasks:
|
||||||
|
|||||||
@@ -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)
|
- [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)
|
||||||
@@ -371,7 +372,29 @@ body: '{"ip": "{{ fakeit_IPv4Address }}", "timestamp": "{{ fakeit_Date }}", "act
|
|||||||
|
|
||||||
</details>
|
</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
|
## Request Bodies
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +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:** 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)
|
||||||
|
- [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)
|
||||||
@@ -109,6 +118,51 @@ 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
|
||||||
|
|
||||||
|
| Function | Description | Example |
|
||||||
|
| ------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------- |
|
||||||
|
| `time_NowUnix` | Current Unix timestamp (seconds) | `{{ time_NowUnix }}` → `1735689600` |
|
||||||
|
| `time_NowUnixMilli` | Current Unix timestamp (milliseconds) | `{{ time_NowUnixMilli }}` → `1735689600123` |
|
||||||
|
| `time_NowRFC3339` | Current time in RFC3339 format | `{{ time_NowRFC3339 }}` → `"2026-02-26T21:00:00Z"` |
|
||||||
|
| `time_Format(layout, t)` | Format a `time.Time` value with a Go layout | `{{ time_Format "2006-01-02" (strings_ToDate "2024-05-10") }}` → `"2024-05-10"` |
|
||||||
|
|
||||||
|
### Crypto Functions
|
||||||
|
|
||||||
|
| Function | Description | Example |
|
||||||
|
| ------------------------------------ | ------------------------------------------ | -------------------------------------------- |
|
||||||
|
| `crypto_SHA256(s string)` | SHA-256 hash (hex-encoded) | `{{ crypto_SHA256 "hello" }}` |
|
||||||
|
| `crypto_MD5(s string)` | MD5 hash (hex-encoded) | `{{ crypto_MD5 "hello" }}` |
|
||||||
|
| `crypto_HMACSHA256(key, msg string)` | HMAC-SHA256 signature (hex-encoded) | `{{ crypto_HMACSHA256 "secret" "payload" }}` |
|
||||||
|
| `crypto_Base64URL(s string)` | Base64 URL-safe encoding (without padding) | `{{ crypto_Base64URL "hello world" }}` |
|
||||||
|
|
||||||
### Body Functions
|
### Body Functions
|
||||||
|
|
||||||
| Function | Description | Example |
|
| Function | Description | Example |
|
||||||
@@ -153,11 +207,18 @@ body: '{{ body_FormData "twitter" "@@username" }}'
|
|||||||
|
|
||||||
| Function | Description | Example |
|
| Function | Description | Example |
|
||||||
| ---------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
| ---------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------- |
|
||||||
|
| `file_Read(source string)` | Read a file (local path or URL) and return raw content as string. Files are cached after first read. | `{{ file_Read "/path/to/file.txt" }}` |
|
||||||
| `file_Base64(source string)` | Read a file (local path or URL) and return its Base64 encoded content. Files are cached after first read. | `{{ file_Base64 "/path/to/file.pdf" }}` |
|
| `file_Base64(source string)` | Read a file (local path or URL) and return its Base64 encoded content. Files are cached after first read. | `{{ file_Base64 "/path/to/file.pdf" }}` |
|
||||||
|
|
||||||
**`file_Base64` Details:**
|
**`file_Read` and `file_Base64` Details:**
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
# Local file as plain text
|
||||||
|
body: '{{ file_Read "/path/to/template.json" }}'
|
||||||
|
|
||||||
|
# Remote text file
|
||||||
|
body: '{{ file_Read "https://example.com/payload.txt" }}'
|
||||||
|
|
||||||
# Local file as Base64 in JSON body
|
# Local file as Base64 in JSON body
|
||||||
body: '{"file": "{{ file_Base64 "/path/to/document.pdf" }}", "filename": "document.pdf"}'
|
body: '{"file": "{{ file_Base64 "/path/to/document.pdf" }}", "filename": "document.pdf"}'
|
||||||
|
|
||||||
@@ -169,6 +230,95 @@ 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.
|
||||||
|
|||||||
47
go.mod
47
go.mod
@@ -1,47 +1,46 @@
|
|||||||
module go.aykhans.me/sarin
|
module go.aykhans.me/sarin
|
||||||
|
|
||||||
go 1.26.0
|
go 1.26.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/brianvoe/gofakeit/v7 v7.14.0
|
github.com/brianvoe/gofakeit/v7 v7.14.1
|
||||||
github.com/charmbracelet/bubbles v1.0.0
|
github.com/charmbracelet/bubbles v1.0.0
|
||||||
github.com/charmbracelet/bubbletea v1.3.10
|
github.com/charmbracelet/bubbletea v1.3.10
|
||||||
github.com/charmbracelet/glamour v0.10.0
|
github.com/charmbracelet/glamour v1.0.0
|
||||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834
|
||||||
github.com/charmbracelet/x/term v0.2.2
|
github.com/charmbracelet/x/term v0.2.2
|
||||||
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3
|
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
github.com/valyala/fasthttp v1.69.0
|
github.com/valyala/fasthttp v1.70.0
|
||||||
github.com/yuin/gopher-lua v1.1.1
|
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.3
|
go.yaml.in/yaml/v4 v4.0.0-rc.4
|
||||||
golang.org/x/net v0.50.0
|
golang.org/x/net v0.52.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/alecthomas/chroma/v2 v2.21.1 // indirect
|
github.com/alecthomas/chroma/v2 v2.23.1 // indirect
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||||
github.com/aymerick/douceur v0.2.0 // indirect
|
github.com/aymerick/douceur v0.2.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.4.1 // indirect
|
github.com/charmbracelet/colorprofile v0.4.3 // indirect
|
||||||
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
github.com/charmbracelet/harmonica v0.2.0 // indirect
|
||||||
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
github.com/charmbracelet/x/ansi v0.11.6 // indirect
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
|
||||||
github.com/charmbracelet/x/exp/slice v0.0.0-20260109001716-2fbdffcb221f // indirect
|
github.com/charmbracelet/x/exp/slice v0.0.0-20260329003944-7eda8903d971 // indirect
|
||||||
github.com/clipperhouse/displaywidth v0.9.0 // indirect
|
github.com/clipperhouse/displaywidth v0.11.0 // indirect
|
||||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||||
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
|
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc // indirect
|
||||||
github.com/gorilla/css v1.0.1 // indirect
|
github.com/gorilla/css v1.0.1 // indirect
|
||||||
github.com/klauspost/compress v1.18.2 // indirect
|
github.com/klauspost/compress v1.18.5 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
github.com/mattn/go-runewidth v0.0.21 // indirect
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||||
@@ -51,9 +50,9 @@ require (
|
|||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
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.7.16 // 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.41.0 // indirect
|
golang.org/x/sys v0.42.0 // indirect
|
||||||
golang.org/x/term v0.40.0 // indirect
|
golang.org/x/term v0.41.0 // indirect
|
||||||
golang.org/x/text v0.34.0 // indirect
|
golang.org/x/text v0.35.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
90
go.sum
90
go.sum
@@ -2,28 +2,28 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0
|
|||||||
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
github.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=
|
||||||
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
github.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=
|
||||||
github.com/alecthomas/chroma/v2 v2.21.1 h1:FaSDrp6N+3pphkNKU6HPCiYLgm8dbe5UXIXcoBhZSWA=
|
github.com/alecthomas/chroma/v2 v2.23.1 h1:nv2AVZdTyClGbVQkIzlDm/rnhk1E9bU9nXwmZ/Vk/iY=
|
||||||
github.com/alecthomas/chroma/v2 v2.21.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
github.com/alecthomas/chroma/v2 v2.23.1/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.1 h1:R+f5xP285VArJDRgowrfb9DqL18yVK0gKAW/F+eTWro=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.1/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||||
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
|
||||||
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
|
||||||
github.com/brianvoe/gofakeit/v7 v7.14.0 h1:R8tmT/rTDJmD2ngpqBL9rAKydiL7Qr2u3CXPqRt59pk=
|
github.com/brianvoe/gofakeit/v7 v7.14.1 h1:a7fe3fonbj0cW3wgl5VwIKfZtiH9C3cLnwcIXWT7sow=
|
||||||
github.com/brianvoe/gofakeit/v7 v7.14.0/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
|
github.com/brianvoe/gofakeit/v7 v7.14.1/go.mod h1:QXuPeBw164PJCzCUZVmgpgHJ3Llj49jSLVkKPMtxtxA=
|
||||||
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
|
||||||
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||||
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
|
github.com/charmbracelet/colorprofile v0.4.3 h1:QPa1IWkYI+AOB+fE+mg/5/4HRMZcaXex9t5KX76i20Q=
|
||||||
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
|
github.com/charmbracelet/colorprofile v0.4.3/go.mod h1:/zT4BhpD5aGFpqQQqw7a+VtHCzu+zrQtt1zhMt9mR4Q=
|
||||||
github.com/charmbracelet/glamour v0.10.0 h1:MtZvfwsYCx8jEPFJm3rIBFIMZUfUJ765oX8V6kXldcY=
|
github.com/charmbracelet/glamour v1.0.0 h1:AWMLOVFHTsysl4WV8T8QgkQ0s/ZNZo7CiE4WKhk8l08=
|
||||||
github.com/charmbracelet/glamour v0.10.0/go.mod h1:f+uf+I/ChNmqo087elLnVdCiVgjSKWuXa/l6NU2ndYk=
|
github.com/charmbracelet/glamour v1.0.0/go.mod h1:DSdohgOBkMr2ZQNhw4LZxSGpx3SvpeujNoXrQyH2hxo=
|
||||||
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ=
|
||||||
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao=
|
||||||
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 h1:ZR7e0ro+SZZiIZD7msJyA+NjkCNNavuiPBLgerbOziE=
|
||||||
@@ -34,45 +34,43 @@ github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMx
|
|||||||
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ=
|
||||||
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U=
|
||||||
github.com/charmbracelet/x/exp/slice v0.0.0-20260109001716-2fbdffcb221f h1:kvAY8ffwhFuxWqtVI6+9E5vmgTApG96hswFLXJfsxHI=
|
github.com/charmbracelet/x/exp/slice v0.0.0-20260329003944-7eda8903d971 h1:wae/9jUCdhUiuyCcOzZZ+vJEB7uJx+IvtTnpCqcW1ZQ=
|
||||||
github.com/charmbracelet/x/exp/slice v0.0.0-20260109001716-2fbdffcb221f/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
|
github.com/charmbracelet/x/exp/slice v0.0.0-20260329003944-7eda8903d971/go.mod h1:vqEfX6xzqW1pKKZUUiFOKg0OQ7bCh54Q2vR/tserrRA=
|
||||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||||
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
|
github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8=
|
||||||
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
|
github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0=
|
||||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||||
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
|
|
||||||
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
|
||||||
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||||
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3 h1:bVp3yUzvSAJzu9GqID+Z96P+eu5TKnIMJSV4QaZMauM=
|
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c h1:OcLmPfx1T1RmZVHHFwWMPaZDdRf0DBMZOFMVWJa7Pdk=
|
||||||
github.com/dop251/goja v0.0.0-20260106131823-651366fbe6e3/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
github.com/dop251/goja v0.0.0-20260311135729-065cd970411c/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
|
||||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
|
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=
|
||||||
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
|
github.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=
|
||||||
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
|
||||||
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
|
||||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
|
||||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
github.com/lucasb-eyer/go-colorful v1.4.0 h1:UtrWVfLdarDgc44HcS7pYloGHJUjHV/4FwW4TvVgFr4=
|
||||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
github.com/lucasb-eyer/go-colorful v1.4.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||||
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
|
||||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
|
||||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||||
@@ -93,34 +91,34 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
|
|||||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasthttp v1.69.0 h1:fNLLESD2SooWeh2cidsuFtOcrEi4uB4m1mPrkJMZyVI=
|
github.com/valyala/fasthttp v1.70.0 h1:LAhMGcWk13QZWm85+eg8ZBNbrq5mnkWFGbHMUJHIdXA=
|
||||||
github.com/valyala/fasthttp v1.69.0/go.mod h1:4wA4PfAraPlAsJ5jMSqCE2ug5tqUPwKXxVj8oNECGcw=
|
github.com/valyala/fasthttp v1.70.0/go.mod h1:oDZEHHkJ/Buyklg6uURmYs19442zFSnCIfX3j1FY3pE=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||||
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
github.com/yuin/goldmark-emoji v1.0.6 h1:QWfF2FYaXwL74tfGOW5izeiZepUDroDJfWubQI9HTHs=
|
||||||
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
github.com/yuin/goldmark-emoji v1.0.6/go.mod h1:ukxJDKFpdFb5x0a5HqbdlcKtebh086iJpI31LTKmWuA=
|
||||||
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
|
github.com/yuin/gopher-lua v1.1.2 h1:yF/FjE3hD65tBbt0VXLE13HWS9h34fdzJmrWRXwobGA=
|
||||||
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
|
github.com/yuin/gopher-lua v1.1.2/go.mod h1:7aRmXIWl37SqRf0koeyylBEzJ+aPt8A+mmkQ4f1ntR8=
|
||||||
go.aykhans.me/utils v1.0.7 h1:ClHXHlWmkjfFlD7+w5BQY29lKCEztxY/yCf543x4hZw=
|
go.aykhans.me/utils v1.0.7 h1:ClHXHlWmkjfFlD7+w5BQY29lKCEztxY/yCf543x4hZw=
|
||||||
go.aykhans.me/utils v1.0.7/go.mod h1:0Jz8GlZLN35cCHLOLx39sazWwEe33bF6SYlSeqzEXoI=
|
go.aykhans.me/utils v1.0.7/go.mod h1:0Jz8GlZLN35cCHLOLx39sazWwEe33bF6SYlSeqzEXoI=
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
go.yaml.in/yaml/v4 v4.0.0-rc.4 h1:UP4+v6fFrBIb1l934bDl//mmnoIZEDK0idg1+AIvX5U=
|
||||||
go.yaml.in/yaml/v4 v4.0.0-rc.3/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.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||||
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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||||
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
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=
|
||||||
|
|||||||
@@ -418,7 +418,7 @@ func (config Config) Validate() error {
|
|||||||
validationErrors = append(validationErrors, types.NewFieldValidationError("Duration", "0", errors.New("duration must be greater than 0")))
|
validationErrors = append(validationErrors, types.NewFieldValidationError("Duration", "0", errors.New("duration must be greater than 0")))
|
||||||
}
|
}
|
||||||
|
|
||||||
if *config.Timeout < 1 {
|
if config.Timeout == nil || *config.Timeout < 1 {
|
||||||
validationErrors = append(validationErrors, types.NewFieldValidationError("Timeout", "0", errors.New("timeout must be greater than 0")))
|
validationErrors = append(validationErrors, types.NewFieldValidationError("Timeout", "0", errors.New("timeout must be greater than 0")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ func (parser ConfigENVParser) Parse() (*Config, error) {
|
|||||||
types.NewFieldParseError(
|
types.NewFieldParseError(
|
||||||
parser.getFullEnvName("DURATION"),
|
parser.getFullEnvName("DURATION"),
|
||||||
duration,
|
duration,
|
||||||
errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"),
|
errors.New("invalid value for duration, expected a duration string (e.g., '10s', '1h30m')"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -173,7 +173,7 @@ func (parser ConfigENVParser) Parse() (*Config, error) {
|
|||||||
types.NewFieldParseError(
|
types.NewFieldParseError(
|
||||||
parser.getFullEnvName("TIMEOUT"),
|
parser.getFullEnvName("TIMEOUT"),
|
||||||
timeout,
|
timeout,
|
||||||
errors.New("invalid value duration, expected a duration string (e.g., '10s', '1h30m')"),
|
errors.New("invalid value for duration, expected a duration string (e.g., '10s', '1h30m')"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
415
internal/sarin/captcha.go
Normal file
415
internal/sarin/captcha.go
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -172,7 +172,6 @@ func fasthttpSocksDialerDualStackTimeout(ctx context.Context, proxyURL *url.URL,
|
|||||||
return nil, types.NewProxyDialError(proxyStr, err)
|
return nil, types.NewProxyDialError(proxyStr, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cap DNS resolution to half the timeout to reserve time for dial
|
|
||||||
dnsCtx, dnsCancel := context.WithTimeout(ctx, timeout)
|
dnsCtx, dnsCancel := context.WithTimeout(ctx, timeout)
|
||||||
ips, err := net.DefaultResolver.LookupIP(dnsCtx, "ip", host)
|
ips, err := net.DefaultResolver.LookupIP(dnsCtx, "ip", host)
|
||||||
dnsCancel()
|
dnsCancel()
|
||||||
@@ -244,7 +243,7 @@ func fasthttpHTTPSDialerDualStackTimeout(proxyURL *url.URL, timeout time.Duratio
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Upgrade to TLS
|
// Upgrade to TLS
|
||||||
tlsConn := tls.Client(conn, &tls.Config{ //nolint:gosec
|
tlsConn := tls.Client(conn, &tls.Config{
|
||||||
ServerName: proxyURL.Hostname(),
|
ServerName: proxyURL.Hostname(),
|
||||||
})
|
})
|
||||||
if err := tlsConn.Handshake(); err != nil {
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
|
|||||||
@@ -8,7 +8,14 @@ import (
|
|||||||
func NewDefaultRandSource() rand.Source {
|
func NewDefaultRandSource() rand.Source {
|
||||||
now := time.Now().UnixNano()
|
now := time.Now().UnixNano()
|
||||||
return rand.NewPCG(
|
return rand.NewPCG(
|
||||||
uint64(now), //nolint:gosec // G115: Safe conversion; UnixNano timestamp used as random seed, bit pattern is intentional
|
uint64(now),
|
||||||
uint64(now>>32), //nolint:gosec // G115: Safe conversion; right-shifted timestamp for seed entropy, overflow is acceptable
|
uint64(now>>32),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func firstOrEmpty(values []string) string {
|
||||||
|
if len(values) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return values[0]
|
||||||
|
}
|
||||||
|
|||||||
@@ -43,19 +43,34 @@ func NewRequestGenerator(
|
|||||||
randSource := NewDefaultRandSource()
|
randSource := NewDefaultRandSource()
|
||||||
//nolint:gosec // G404: Using non-cryptographic rand for load testing, not security
|
//nolint:gosec // G404: Using non-cryptographic rand for load testing, not security
|
||||||
localRand := rand.New(randSource)
|
localRand := rand.New(randSource)
|
||||||
templateFuncMap := NewDefaultTemplateFuncMap(randSource, fileCache)
|
|
||||||
|
|
||||||
pathGenerator, isPathGeneratorDynamic := createTemplateFunc(requestURL.Path, templateFuncMap)
|
// Funcs() is only called if a value actually contains template syntax.
|
||||||
methodGenerator, isMethodGeneratorDynamic := NewMethodGeneratorFunc(localRand, methods, templateFuncMap)
|
// The root template is shared across all createTemplateFunc calls so Funcs() is called at most once.
|
||||||
paramsGenerator, isParamsGeneratorDynamic := NewParamsGeneratorFunc(localRand, params, templateFuncMap)
|
var templateRoot *template.Template
|
||||||
headersGenerator, isHeadersGeneratorDynamic := NewHeadersGeneratorFunc(localRand, headers, templateFuncMap)
|
lazyTemplateRoot := func() *template.Template {
|
||||||
cookiesGenerator, isCookiesGeneratorDynamic := NewCookiesGeneratorFunc(localRand, cookies, templateFuncMap)
|
if templateRoot == nil {
|
||||||
|
templateRoot = template.New("").Funcs(NewDefaultTemplateFuncMap(randSource, fileCache))
|
||||||
|
}
|
||||||
|
return templateRoot
|
||||||
|
}
|
||||||
|
|
||||||
|
pathGenerator, isPathGeneratorDynamic := createTemplateFunc(requestURL.Path, lazyTemplateRoot)
|
||||||
|
methodGenerator, isMethodGeneratorDynamic := NewMethodGeneratorFunc(localRand, methods, lazyTemplateRoot)
|
||||||
|
paramsGenerator, isParamsGeneratorDynamic := NewParamsGeneratorFunc(localRand, params, lazyTemplateRoot)
|
||||||
|
headersGenerator, isHeadersGeneratorDynamic := NewHeadersGeneratorFunc(localRand, headers, lazyTemplateRoot)
|
||||||
|
cookiesGenerator, isCookiesGeneratorDynamic := NewCookiesGeneratorFunc(localRand, cookies, lazyTemplateRoot)
|
||||||
|
|
||||||
bodyTemplateFuncMapData := &BodyTemplateFuncMapData{}
|
bodyTemplateFuncMapData := &BodyTemplateFuncMapData{}
|
||||||
bodyTemplateFuncMap := NewDefaultBodyTemplateFuncMap(randSource, bodyTemplateFuncMapData, fileCache)
|
var bodyTemplateRoot *template.Template
|
||||||
bodyGenerator, isBodyGeneratorDynamic := NewBodyGeneratorFunc(localRand, bodies, bodyTemplateFuncMap)
|
lazyBodyTemplateRoot := func() *template.Template {
|
||||||
|
if bodyTemplateRoot == nil {
|
||||||
|
bodyTemplateRoot = template.New("").Funcs(NewDefaultBodyTemplateFuncMap(randSource, bodyTemplateFuncMapData, fileCache))
|
||||||
|
}
|
||||||
|
return bodyTemplateRoot
|
||||||
|
}
|
||||||
|
bodyGenerator, isBodyGeneratorDynamic := NewBodyGeneratorFunc(localRand, bodies, lazyBodyTemplateRoot)
|
||||||
|
|
||||||
valuesGenerator := NewValuesGeneratorFunc(values, templateFuncMap)
|
valuesGenerator := NewValuesGeneratorFunc(values, lazyTemplateRoot)
|
||||||
|
|
||||||
hasScripts := scriptTransformer != nil && !scriptTransformer.IsEmpty()
|
hasScripts := scriptTransformer != nil && !scriptTransformer.IsEmpty()
|
||||||
|
|
||||||
@@ -91,7 +106,7 @@ func NewRequestGenerator(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyTemplateFuncMapData.ClearFormDataContenType()
|
bodyTemplateFuncMapData.ClearFormDataContentType()
|
||||||
if err = bodyGenerator(reqData, data); err != nil {
|
if err = bodyGenerator(reqData, data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -99,8 +114,8 @@ func NewRequestGenerator(
|
|||||||
if err = headersGenerator(reqData, data); err != nil {
|
if err = headersGenerator(reqData, data); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if bodyTemplateFuncMapData.GetFormDataContenType() != "" {
|
if bodyTemplateFuncMapData.GetFormDataContentType() != "" {
|
||||||
reqData.Headers["Content-Type"] = append(reqData.Headers["Content-Type"], bodyTemplateFuncMapData.GetFormDataContenType())
|
reqData.Headers["Content-Type"] = append(reqData.Headers["Content-Type"], bodyTemplateFuncMapData.GetFormDataContentType())
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = paramsGenerator(reqData, data); err != nil {
|
if err = paramsGenerator(reqData, data); err != nil {
|
||||||
@@ -170,8 +185,8 @@ func applyRequestDataToFastHTTP(reqData *script.RequestData, req *fasthttp.Reque
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMethodGeneratorFunc(localRand *rand.Rand, methods []string, templateFunctions template.FuncMap) (requestDataGenerator, bool) {
|
func NewMethodGeneratorFunc(localRand *rand.Rand, methods []string, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
||||||
methodGenerator, isDynamic := buildStringSliceGenerator(localRand, methods, templateFunctions)
|
methodGenerator, isDynamic := buildStringSliceGenerator(localRand, methods, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
method string
|
method string
|
||||||
@@ -188,8 +203,8 @@ func NewMethodGeneratorFunc(localRand *rand.Rand, methods []string, templateFunc
|
|||||||
}, isDynamic
|
}, isDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBodyGeneratorFunc(localRand *rand.Rand, bodies []string, templateFunctions template.FuncMap) (requestDataGenerator, bool) {
|
func NewBodyGeneratorFunc(localRand *rand.Rand, bodies []string, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
||||||
bodyGenerator, isDynamic := buildStringSliceGenerator(localRand, bodies, templateFunctions)
|
bodyGenerator, isDynamic := buildStringSliceGenerator(localRand, bodies, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
body string
|
body string
|
||||||
@@ -206,8 +221,8 @@ func NewBodyGeneratorFunc(localRand *rand.Rand, bodies []string, templateFunctio
|
|||||||
}, isDynamic
|
}, isDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewParamsGeneratorFunc(localRand *rand.Rand, params types.Params, templateFunctions template.FuncMap) (requestDataGenerator, bool) {
|
func NewParamsGeneratorFunc(localRand *rand.Rand, params types.Params, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
||||||
generators, isDynamic := buildKeyValueGenerators(localRand, params, templateFunctions)
|
generators, isDynamic := buildKeyValueGenerators(localRand, params, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key, value string
|
key, value string
|
||||||
@@ -231,8 +246,8 @@ func NewParamsGeneratorFunc(localRand *rand.Rand, params types.Params, templateF
|
|||||||
}, isDynamic
|
}, isDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHeadersGeneratorFunc(localRand *rand.Rand, headers types.Headers, templateFunctions template.FuncMap) (requestDataGenerator, bool) {
|
func NewHeadersGeneratorFunc(localRand *rand.Rand, headers types.Headers, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
||||||
generators, isDynamic := buildKeyValueGenerators(localRand, headers, templateFunctions)
|
generators, isDynamic := buildKeyValueGenerators(localRand, headers, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key, value string
|
key, value string
|
||||||
@@ -256,8 +271,8 @@ func NewHeadersGeneratorFunc(localRand *rand.Rand, headers types.Headers, templa
|
|||||||
}, isDynamic
|
}, isDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCookiesGeneratorFunc(localRand *rand.Rand, cookies types.Cookies, templateFunctions template.FuncMap) (requestDataGenerator, bool) {
|
func NewCookiesGeneratorFunc(localRand *rand.Rand, cookies types.Cookies, lazyRoot func() *template.Template) (requestDataGenerator, bool) {
|
||||||
generators, isDynamic := buildKeyValueGenerators(localRand, cookies, templateFunctions)
|
generators, isDynamic := buildKeyValueGenerators(localRand, cookies, lazyRoot)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
key, value string
|
key, value string
|
||||||
@@ -281,11 +296,11 @@ func NewCookiesGeneratorFunc(localRand *rand.Rand, cookies types.Cookies, templa
|
|||||||
}, isDynamic
|
}, isDynamic
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewValuesGeneratorFunc(values []string, templateFunctions template.FuncMap) func() (valuesData, error) {
|
func NewValuesGeneratorFunc(values []string, lazyRoot func() *template.Template) func() (valuesData, error) {
|
||||||
generators := make([]func(_ any) (string, error), len(values))
|
generators := make([]func(_ any) (string, error), len(values))
|
||||||
|
|
||||||
for i, v := range values {
|
for i, v := range values {
|
||||||
generators[i], _ = createTemplateFunc(v, templateFunctions)
|
generators[i], _ = createTemplateFunc(v, lazyRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -313,8 +328,12 @@ func NewValuesGeneratorFunc(values []string, templateFunctions template.FuncMap)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTemplateFunc(value string, templateFunctions template.FuncMap) (func(data any) (string, error), bool) {
|
func createTemplateFunc(value string, lazyRoot func() *template.Template) (func(data any) (string, error), bool) {
|
||||||
tmpl, err := template.New("").Funcs(templateFunctions).Parse(value)
|
if !strings.Contains(value, "{{") {
|
||||||
|
return func(_ any) (string, error) { return value, nil }, false
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl, err := lazyRoot().New("").Parse(value)
|
||||||
if err == nil && hasTemplateActions(tmpl) {
|
if err == nil && hasTemplateActions(tmpl) {
|
||||||
var err error
|
var err error
|
||||||
return func(data any) (string, error) {
|
return func(data any) (string, error) {
|
||||||
@@ -340,7 +359,7 @@ type keyValueItem interface {
|
|||||||
func buildKeyValueGenerators[T keyValueItem](
|
func buildKeyValueGenerators[T keyValueItem](
|
||||||
localRand *rand.Rand,
|
localRand *rand.Rand,
|
||||||
items []T,
|
items []T,
|
||||||
templateFunctions template.FuncMap,
|
lazyRoot func() *template.Template,
|
||||||
) ([]keyValueGenerator, bool) {
|
) ([]keyValueGenerator, bool) {
|
||||||
isDynamic := false
|
isDynamic := false
|
||||||
generators := make([]keyValueGenerator, len(items))
|
generators := make([]keyValueGenerator, len(items))
|
||||||
@@ -350,7 +369,7 @@ func buildKeyValueGenerators[T keyValueItem](
|
|||||||
keyValue := types.KeyValue[string, []string](item)
|
keyValue := types.KeyValue[string, []string](item)
|
||||||
|
|
||||||
// Generate key function
|
// Generate key function
|
||||||
keyFunc, keyIsDynamic := createTemplateFunc(keyValue.Key, templateFunctions)
|
keyFunc, keyIsDynamic := createTemplateFunc(keyValue.Key, lazyRoot)
|
||||||
if keyIsDynamic {
|
if keyIsDynamic {
|
||||||
isDynamic = true
|
isDynamic = true
|
||||||
}
|
}
|
||||||
@@ -358,7 +377,7 @@ func buildKeyValueGenerators[T keyValueItem](
|
|||||||
// Generate value functions
|
// Generate value functions
|
||||||
valueFuncs := make([]func(data any) (string, error), len(keyValue.Value))
|
valueFuncs := make([]func(data any) (string, error), len(keyValue.Value))
|
||||||
for j, v := range keyValue.Value {
|
for j, v := range keyValue.Value {
|
||||||
valueFunc, valueIsDynamic := createTemplateFunc(v, templateFunctions)
|
valueFunc, valueIsDynamic := createTemplateFunc(v, lazyRoot)
|
||||||
if valueIsDynamic {
|
if valueIsDynamic {
|
||||||
isDynamic = true
|
isDynamic = true
|
||||||
}
|
}
|
||||||
@@ -381,7 +400,7 @@ func buildKeyValueGenerators[T keyValueItem](
|
|||||||
func buildStringSliceGenerator(
|
func buildStringSliceGenerator(
|
||||||
localRand *rand.Rand,
|
localRand *rand.Rand,
|
||||||
values []string,
|
values []string,
|
||||||
templateFunctions template.FuncMap,
|
lazyRoot func() *template.Template,
|
||||||
) (func() func(data any) (string, error), bool) {
|
) (func() func(data any) (string, error), bool) {
|
||||||
// Return a function that returns an empty string generator if values is empty
|
// Return a function that returns an empty string generator if values is empty
|
||||||
if len(values) == 0 {
|
if len(values) == 0 {
|
||||||
@@ -393,7 +412,7 @@ func buildStringSliceGenerator(
|
|||||||
valueFuncs := make([]func(data any) (string, error), len(values))
|
valueFuncs := make([]func(data any) (string, error), len(values))
|
||||||
|
|
||||||
for i, value := range values {
|
for i, value := range values {
|
||||||
valueFunc, valueIsDynamic := createTemplateFunc(value, templateFunctions)
|
valueFunc, valueIsDynamic := createTemplateFunc(value, lazyRoot)
|
||||||
if valueIsDynamic {
|
if valueIsDynamic {
|
||||||
isDynamic = true
|
isDynamic = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -484,13 +484,11 @@ func newHostClients(
|
|||||||
proxiesRaw[i] = url.URL(proxy)
|
proxiesRaw[i] = url.URL(proxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
maxConns := max(fasthttp.DefaultMaxConnsPerHost, workers)
|
|
||||||
maxConns = ((maxConns * 50 / 100) + maxConns)
|
|
||||||
return NewHostClients(
|
return NewHostClients(
|
||||||
ctx,
|
ctx,
|
||||||
timeout,
|
timeout,
|
||||||
proxiesRaw,
|
proxiesRaw,
|
||||||
maxConns,
|
workers,
|
||||||
requestURL,
|
requestURL,
|
||||||
skipCertVerify,
|
skipCertVerify,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ package sarin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/md5" // #nosec G501 -- exposed intentionally as a template utility helper
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -81,7 +86,79 @@ 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_NowUnix": func() int64 { return time.Now().Unix() },
|
||||||
|
"time_NowUnixMilli": func() int64 { return time.Now().UnixMilli() },
|
||||||
|
"time_NowRFC3339": func() string { return time.Now().Format(time.RFC3339) },
|
||||||
|
"time_Format": func(layout string, t time.Time) string {
|
||||||
|
return t.Format(layout)
|
||||||
|
},
|
||||||
|
|
||||||
|
// Crypto
|
||||||
|
"crypto_SHA256": func(s string) string {
|
||||||
|
sum := sha256.Sum256([]byte(s))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
},
|
||||||
|
"crypto_MD5": func(s string) string {
|
||||||
|
sum := md5.Sum([]byte(s)) // #nosec G401 -- MD5 is intentionally provided as a non-security template helper
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
|
},
|
||||||
|
"crypto_HMACSHA256": func(key string, msg string) string {
|
||||||
|
mac := hmac.New(sha256.New, []byte(key))
|
||||||
|
_, _ = mac.Write([]byte(msg))
|
||||||
|
return hex.EncodeToString(mac.Sum(nil))
|
||||||
|
},
|
||||||
|
"crypto_Base64URL": func(s string) string {
|
||||||
|
return base64.RawURLEncoding.EncodeToString([]byte(s))
|
||||||
|
},
|
||||||
|
|
||||||
// File
|
// File
|
||||||
|
// file_Read reads a file (local or remote URL) and returns its content as a string.
|
||||||
|
// Usage: {{ file_Read "/path/to/file.txt" }}
|
||||||
|
// {{ file_Read "https://example.com/data.txt" }}
|
||||||
|
"file_Read": func(source string) (string, error) {
|
||||||
|
if fileCache == nil {
|
||||||
|
return "", types.ErrFileCacheNotInitialized
|
||||||
|
}
|
||||||
|
cached, err := fileCache.GetOrLoad(source)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(cached.Content), nil
|
||||||
|
},
|
||||||
|
|
||||||
// file_Base64 reads a file (local or remote URL) and returns its Base64 encoded content.
|
// file_Base64 reads a file (local or remote URL) and returns its Base64 encoded content.
|
||||||
// Usage: {{ file_Base64 "/path/to/file.pdf" }}
|
// Usage: {{ file_Base64 "/path/to/file.pdf" }}
|
||||||
// {{ file_Base64 "https://example.com/image.png" }}
|
// {{ file_Base64 "https://example.com/image.png" }}
|
||||||
@@ -242,7 +319,7 @@ func NewDefaultTemplateFuncMap(randSource rand.Source, fileCache *FileCache) tem
|
|||||||
"fakeit_AdverbFrequencyDefinite": fakeit.AdverbFrequencyDefinite,
|
"fakeit_AdverbFrequencyDefinite": fakeit.AdverbFrequencyDefinite,
|
||||||
"fakeit_AdverbFrequencyIndefinite": fakeit.AdverbFrequencyIndefinite,
|
"fakeit_AdverbFrequencyIndefinite": fakeit.AdverbFrequencyIndefinite,
|
||||||
|
|
||||||
// Propositions
|
// Prepositions
|
||||||
"fakeit_Preposition": fakeit.Preposition,
|
"fakeit_Preposition": fakeit.Preposition,
|
||||||
"fakeit_PrepositionSimple": fakeit.PrepositionSimple,
|
"fakeit_PrepositionSimple": fakeit.PrepositionSimple,
|
||||||
"fakeit_PrepositionDouble": fakeit.PrepositionDouble,
|
"fakeit_PrepositionDouble": fakeit.PrepositionDouble,
|
||||||
@@ -530,8 +607,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
|
||||||
"fakeit_School": fakeit.School,
|
"fakeit_School": fakeit.School,
|
||||||
@@ -541,19 +617,68 @@ 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))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type BodyTemplateFuncMapData struct {
|
type BodyTemplateFuncMapData struct {
|
||||||
formDataContenType string
|
formDataContentType string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data BodyTemplateFuncMapData) GetFormDataContenType() string {
|
func (data BodyTemplateFuncMapData) GetFormDataContentType() string {
|
||||||
return data.formDataContenType
|
return data.formDataContentType
|
||||||
}
|
}
|
||||||
|
|
||||||
func (data *BodyTemplateFuncMapData) ClearFormDataContenType() {
|
func (data *BodyTemplateFuncMapData) ClearFormDataContentType() {
|
||||||
data.formDataContenType = ""
|
data.formDataContentType = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewDefaultBodyTemplateFuncMap(
|
func NewDefaultBodyTemplateFuncMap(
|
||||||
@@ -584,7 +709,7 @@ func NewDefaultBodyTemplateFuncMap(
|
|||||||
|
|
||||||
var multipartData bytes.Buffer
|
var multipartData bytes.Buffer
|
||||||
writer := multipart.NewWriter(&multipartData)
|
writer := multipart.NewWriter(&multipartData)
|
||||||
data.formDataContenType = writer.FormDataContentType()
|
data.formDataContentType = writer.FormDataContentType()
|
||||||
|
|
||||||
for i := 0; i < len(pairs); i += 2 {
|
for i := 0; i < len(pairs); i += 2 {
|
||||||
key := pairs[i]
|
key := pairs[i]
|
||||||
|
|||||||
@@ -208,8 +208,41 @@ 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
|
||||||
}
|
}
|
||||||
@@ -442,3 +475,91 @@ 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