captchaforge 0.2.21

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers (Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio, sliders).
Documentation
# captchaforge

[![ci](https://github.com/santhsecurity/captchaforge/actions/workflows/ci.yml/badge.svg)](https://github.com/santhsecurity/captchaforge/actions/workflows/ci.yml)
[![license](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)

Automatic CAPTCHA detection and multi-strategy solving for
[`chromiumoxide`](https://crates.io/crates/chromiumoxide)-driven headless
browsers — Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image
grids, audio challenges, sliders, plus a community-extensible TOML rule
pack for vendors like AWS WAF Captcha, DataDome, Akamai Bot Manager,
PerimeterX/HUMAN, Arkose/FunCaptcha, Geetest v3+v4, Friendly Captcha,
MTCaptcha, and WordPress math captchas.

> Part of the [Santh]https://santh.dev security research ecosystem.
> **Beta** — used in production by [wafrift]https://github.com/santhsecurity/wafrift
> and [golemn-browser]https://github.com/santhsecurity/golemn but the
> public API may shift before 1.0. Pin the version in `Cargo.toml`.

## Quick start

Add to `Cargo.toml`:

```toml
[dependencies]
captchaforge = { git = "https://github.com/santhsecurity/captchaforge", branch = "main" }
chromiumoxide = "0.9"
tokio = { version = "1", features = ["full"] }
```

Detect + solve on the page your headless browser is on:

```rust
async fn handle_captcha(page: &chromiumoxide::Page) -> anyhow::Result<()> {
    // One-call convenience: detect, then run the default solver chain
    // if a captcha is present. Returns Ok(None) for no-captcha pages.
    if let Some(outcome) = captchaforge::auto_solve(page).await? {
        if outcome.success {
            tracing::info!(method = ?outcome.method, "captcha solved");
        } else if let Some(screenshot) = outcome.screenshot {
            tracing::warn!(bytes = screenshot.len(), "human review needed; screenshot attached");
        }
    }
    Ok(())
}
```

For full control — telemetry, token cache, custom solvers — drop down
to the explicit chain:

```rust
use std::sync::Arc;
use captchaforge::{detect, solver::{CaptchaSolverChain, TokenCache}};

let cache = Arc::new(TokenCache::new());     // 60s TTL by default
let chain = CaptchaSolverChain::default_chain()
    .with_token_cache(cache);                 // hit cache before solving

let info = detect::detect(page).await?;
if detect::is_captcha(&info) {
    let outcome = chain.solve(page, &info).await;
    // ...
}
```

## Architecture surface (post-extraction)

- **`detect::Detector`** — single per-vendor detector trait. Eleven
  built-ins (`TurnstileDetector`, `RecaptchaDetector`, …) live one per
  file under `src/detect/`. Add a new detector by writing
  `src/detect/<vendor>.rs` and registering it.
- **`detect::rules`** — community-extensible TOML rule layer. Drop a
  `[[provider]]` entry in `rules/community.toml` (or your own file)
  and the vendor is detected without writing any Rust. Bundled pack
  covers DataDome, Akamai, PerimeterX/HUMAN, Arkose/FunCaptcha,
  Geetest v3+v4, AWS WAF Captcha, Friendly Captcha, MTCaptcha,
  WordPress math captchas.
- **`provider::CaptchaProvider`** — bundles a detector + the
  recommended solver methods for that vendor in one trait. Built-in
  providers wired via macro; new providers implement both traits in
  one file.
- **`provider::ProviderRegistry::with_built_in_rules()`** — single
  call returning the built-in detectors PLUS the bundled community
  rule pack, all priority-sorted.
- **`solver::CaptchaSolverChain`** — multi-strategy chain with cache
  + telemetry + pattern-store routing. See "Strategies" below.
- **`solver::TokenCache`** — per-`(domain, captcha_type)` solved
  token cache with configurable TTL. Cache hits skip the solver run
  entirely and report through telemetry as `solver: "TokenCache"`.
- **`telemetry::SolverTelemetry`** — implement to capture every
  solver attempt as a `SolveEvent`. Default is `NoopTelemetry`.
  Production deployments plug in Prometheus / OTel / SQLite / Sentry.
- **`solver::PatternStore`** — per-domain learned routing.
  `save_to_path` / `load_from_path` / `merge` for cross-restart and
  cross-instance sharing.
- **`captchaforge::auto_solve()`** — top-level one-call API for the
  90% case.

## Strategies

For each detected captcha the chain runs through a layered short
circuit + strategy stack. First match wins.

0. **Token cache** — if a `TokenCache` is installed and holds a
   fresh `(domain, captcha_type)` token (default 60s TTL), return it
   immediately tagged as `CrowdSourced`. Telemetry sees this as a
   `Success` with `solver: "TokenCache"`. Skip the rest of the chain.
1. **Provider routing** — for `DetectedCaptcha::Custom` (TOML rule
   layer), only solvers whose `SolveMethod` appears in the bundled
   provider's `recommended_solver_methods` are eligible, in declared
   order. Built-in detectors fall back to the standard `supports()`
   filter.
2. **Pattern store ordering** — for built-in kinds, solvers eligible
   under (1) get re-ordered so the historically-best method for this
   domain runs first. PatternStore can be persisted across restarts
   (`save_to_path` / `load_from_path`) and merged across worker
   instances (`merge`).
3. **Behavioural** — realistic mouse movement + timing for Turnstile
   and reCAPTCHA v3. Solvers verify the response token is actually
   present before reporting `success`. On success, captures session
   cookies into `CaptchaSolveResult::cookies` for replay.
4. **Vision-LLM** — screenshot the challenge, hand it to a multimodal
   model ([Ollama]https://ollama.com `qwen3-vl:30b` out of the box,
   configurable), parse the model's grid / click answer, simulate the
   mouse. Captures session cookies on success.
5. **Audio bypass** — click the accessibility audio challenge, run
   STT, type the transcribed answer, then verify the reCAPTCHA token
   was accepted. Captures session cookies on success.
6. **Telemetry** — every attempt at every layer fires a `SolveEvent`
   to the installed `SolverTelemetry` impl (default `NoopTelemetry`).
   Outcomes: `Success`, `Failure`, `Error`, `Timeout`. Cache hits
   surface as `Success` with `solver: "TokenCache"` so dashboards
   distinguish solved-from-scratch from cache-served.
7. **Human fallback** — when every solver fails, return unsolved
   with a base64 JPEG screenshot in `CaptchaSolveResult::screenshot`
   so a downstream human-in-the-loop can finish.

Apply captured cookies to subsequent navigations via
`captchaforge::cookies::apply_to_page(&page, &result.cookies)` so the
WAF/vendor's trusted session is preserved and the next request does
not re-trigger the captcha.

You can plug in your own solver by implementing the
[`CaptchaSolver`](src/solver.rs) trait and adding it to the chain via
`chain.add_solver(...)` before `default_chain()` strategies.

## Detection coverage

Built-in detectors (one per file under `src/detect/`):

- Cloudflare Turnstile (`.cf-turnstile`, `[data-turnstile-sitekey]`,
  `script[src*="challenges.cloudflare.com"]`)
- Cloudflare bot interstitial ("Just a moment", `#challenge-form`)
- reCAPTCHA v2 + v3 (`.g-recaptcha`, `[data-sitekey]`,
  `script[src*="google.com/recaptcha"]`)
- hCaptcha (`.h-captcha`, `[data-hcaptcha-sitekey]`,
  `script[src*="hcaptcha.com"]`)
- Generic image / audio CAPTCHAs
- PoW CAPTCHAs (ALTCHA, mCaptcha, Friendly Captcha, Cap.dev)
- Slider / swipe / rotate / drag CAPTCHAs
- Canvas / SVG / math / icon / color CAPTCHAs
- Multi-step / wizard CAPTCHAs
- Shadow-DOM-hidden CAPTCHAs

Bundled community rule pack (`rules/community.toml`, registered
via `ProviderRegistry::with_built_in_rules()`):

- AWS WAF Captcha
- DataDome
- Akamai Bot Manager
- PerimeterX / HUMAN
- Arkose Labs / FunCaptcha
- Geetest v3 + v4
- Friendly Captcha
- MTCaptcha
- WordPress math captchas (Captcha by BestWebSoft, generic forms)

Adding a new family is two lines of TOML — see `rules/community.toml`
for the schema. Per-rule positive + negative HTML fixtures live under
`tests/rule_fixtures/<vendor>/`; the `community_rules` integration
test asserts every rule fires on its positive and rejects its
negative AND doesn't cross-fire on any other vendor's negative.

## Configuration

Two layers, lightest to heaviest:

**Env vars (zero-config)**

- `CAPTCHAFORGE_VLM_ENDPOINT` — Ollama endpoint URL.
- `CAPTCHAFORGE_VLM_MODEL` — VLM model name.
- `CAPTCHAFORGE_THIRDPARTY_API_KEY` — 2captcha-compatible service key.
- `CAPTCHAFORGE_THIRDPARTY_ENDPOINT` — override the service base URL.
- `CAPTCHAFORGE_CONFIG` — explicit Tier-A config file path.

**Tier-A operational config (`.captchaforge.toml`)**

Drop `.captchaforge.toml` in your project (or use the discovery search
path: `$CAPTCHAFORGE_CONFIG`, `./.captchaforge.toml`,
`./captchaforge.toml`, `$XDG_CONFIG_HOME/captchaforge/config.toml`).
The schema covers chain timeouts, cache TTL, VLM endpoint/model,
third-party service selection, and every `SolveConfig` field. See
`.captchaforge.toml.example` for the reference. Unknown keys are a
parse error so typos surface instead of silently being ignored.

```toml
per_solver_timeout_ms = 60000

[cache]
ttl_seconds = 90

[vlm]
endpoint = "http://gpu-host:11434"

[third_party]
service = "cap_solver"
api_key = "..."
```

**Builder methods (fine-grained)**

For fine-grained control inside one process, use the builder methods —
they always beat env + TOML.

```rust
use captchaforge::solver::{VlmCaptchaSolver, CaptchaSolverChain};

let mut chain = CaptchaSolverChain::empty();
chain.add_solver(
    VlmCaptchaSolver::new()
        .with_endpoint("http://my-gpu-host:11434")
        .with_model("qwen3-vl:30b"),
);
```

Audio STT endpoint is similarly configurable on `AudioCaptchaSolver`.

**Third-party solver service**

`ThirdPartyCaptchaSolver` talks the 2captcha-compatible HTTP protocol
used by 2captcha, CapMonster, and CapSolver — every TOML-bundled vendor
that recommends `ThirdPartyService` now actually has a solver to land
on. Without an API key configured, `supports()` returns false across
the board so the solver is skipped instead of issuing 401s.

```rust
use captchaforge::solver::{ThirdPartyCaptchaSolver, CaptchaSolverChain};

let mut chain = CaptchaSolverChain::empty();
chain.add_solver(
    // Pick the service flavour, then pass the key:
    ThirdPartyCaptchaSolver::cap_solver().with_api_key("..."),
);
```

## CLI

```sh
cargo install --path cli
captchaforge inspect-rules                         # list bundled + extra rules
captchaforge detect https://example.com/login      # navigate, run detector
captchaforge solve  https://example.com/login      # navigate, run full chain
captchaforge inspect-rules --rules my-pack.toml    # additive extra rules
```

## Telemetry

```rust
use std::sync::Arc;
use captchaforge::solver::CaptchaSolverChain;
use captchaforge::telemetry::{SolveEvent, SolverTelemetry};

struct ToProm;
impl SolverTelemetry for ToProm {
    fn record(&self, evt: &SolveEvent<'_>) {
        // ship evt.outcome, evt.time_ms, evt.solver, evt.domain to your collector
    }
}

let chain = CaptchaSolverChain::default_chain()
    .with_telemetry(Arc::new(ToProm));
```

The chain fires one event per solver attempt at every outcome
(`Success` / `Failure` / `Error` / `Timeout`). Cache hits also fire as
`Success` with `solver: "TokenCache"` so dashboards distinguish
solved-from-scratch from cache-served.

## Origin and licensing

Extracted from [golemn-browser](https://github.com/santhsecurity/golemn)
(originally GPL-3.0). The original author (Santh Project) re-licensed
the extracted slice as MIT OR Apache-2.0 so it can be embedded in
MIT/Apache-licensed downstream tools (notably
[wafrift](https://github.com/santhsecurity/wafrift) for managed-WAF
challenge handling and any future Santh tool that needs a captcha
layer).

## Testing

```sh
cargo test --workspace        # unit + integration (no chromium required)
cargo test --doc              # doctest in lib.rs
cargo test --test community_rules    # asserts every TOML rule fires on its
                              # positive HTML fixture, rejects its negative,
                              # and doesn't cross-fire on other vendors' negatives
cargo test --test integration_chain_wires  # cache + telemetry + chain wires
```

## Local proof (benches)

```sh
cargo run --example architecture_proof --release
```

Prints a markdown report covering the latency-critical paths added by
the architecture sweep:
- TokenCache get hit / miss
- ProviderRegistry::find_by_kind for Custom kinds
- RuleDetector probe generation (one-shot at registration)
- Per-rule HTML fixture match latency (one row per bundled vendor)

Browser-free, runs in well under a second after the release build.
Use to sanity-check that no future change regresses the hot path.

The test suite does **not** require a real chromium binary or a real
Ollama server — all chromium-bound paths are pure Rust unit tests
against the JavaScript / Rust glue layer. Tests that exercise the
real headless browser live in
[`wafrift-captchaforge-bridge`](https://github.com/santhsecurity/wafrift/tree/main/crates/captchaforge-bridge)
where the chromium runtime is available.

## License

MIT OR Apache-2.0 at your option. Copyright 2026 CORUM COLLECTIVE LLC.