captchaforge 0.2.0

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.

> 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
use captchaforge::{detect, solver::CaptchaSolverChain};

async fn handle_captcha(page: &chromiumoxide::Page) -> anyhow::Result<()> {
    let info = detect::detect(page).await?;
    if !detect::is_captcha(&info) {
        return Ok(());
    }

    let chain = CaptchaSolverChain::default_chain();
    let outcome = chain.solve(page, &info).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");
    } else {
        tracing::warn!("all strategies failed; human-in-the-loop needed");
    }
    Ok(())
}
```

## Strategies

The default chain tries strategies in order; the first one that
succeeds wins. Each result is recorded in a per-domain pattern store
so repeat visits prefer the strategy that worked last time.

1. **Behavioural** — realistic mouse movement + timing for Turnstile
   and reCAPTCHA v3. Solvers verify the response token is actually
   present before reporting `success`.
2. **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.
3. **Audio bypass** — click the accessibility audio challenge, run
   STT, type the transcribed answer, then verify the reCAPTCHA token
   was accepted.
4. **Crowd-sourced pattern memory** — track which strategy succeeded
   per-domain and re-prioritise the solver order on next visit.
5. **Human fallback** — return unsolved with a base64 JPEG screenshot
   in `CaptchaSolveResult::screenshot` so a downstream
   human-in-the-loop can finish.

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

Out of the box `detect::detect` recognises:

- 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"]`)
- Image grids, audio challenges, sliders (when the surrounding code
  classifies them via `DetectedCaptcha::*`)

Adding a new family is one match arm in `detect.rs` plus a JavaScript
selector. PRs welcome.

## Configuration

VLM endpoint and model are runtime-overridable on `VlmCaptchaSolver`:

```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`.

## 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          # unit + integration (no chromium required)
cargo test --doc    # doctest in lib.rs
```

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.