captchaforge/lib.rs
1//! captchaforge — automatic CAPTCHA detection and solving for
2//! `chromiumoxide`-driven headless browsers.
3//!
4//! Extracted from [golemn-browser](https://github.com/santhsecurity/golemn)
5//! (originally GPL-3.0) and re-licensed MIT OR Apache-2.0 by the
6//! original author so it can stand alone and be embedded by other
7//! Santh-ecosystem tools (wafrift, gossan, sear, …).
8//!
9//! Two modules:
10//! - [`detect`] — heuristic + DOM-based CAPTCHA identification
11//! - [`solver`] — multi-strategy solving chain
12//! - **Behavioural** — realistic mouse + timing for Turnstile /
13//! reCAPTCHA v3
14//! - **Vision-LLM** — screenshot → multimodal model → click /
15//! type the answer
16//! - **Audio bypass** — accessibility audio challenge → STT →
17//! transcribed answer
18//! - **Crowd-sourced** — per-domain pattern memory of which
19//! method has succeeded before
20//! - **Behavioural / human fallback** — return unsolved for
21//! human-in-the-loop
22//!
23//! The crate exposes a `CaptchaSolver` trait so consumers can plug
24//! in their own solver strategies without forking.
25//!
26//! # Example — detect and solve
27//!
28//! ```rust,no_run
29//! use captchaforge::{detect, solver::CaptchaSolverChain};
30//! # async fn run(page: &chromiumoxide::Page) -> anyhow::Result<()> {
31//! let info = detect::detect(page).await?;
32//! if detect::is_captcha(&info) {
33//! let chain = CaptchaSolverChain::default_chain();
34//! // chain.solve(page, &info).await?;
35//! }
36//! # Ok(()) }
37//! ```
38//!
39//! # Example — pattern store
40//!
41//! ```rust
42//! use captchaforge::solver::{CaptchaType, PatternStore, SolveMethod};
43//!
44//! let store = PatternStore::default();
45//! store.record("example.com", &CaptchaType::CloudflareTurnstile, true, 1200, SolveMethod::BehavioralBypass);
46//! assert_eq!(store.best_method("example.com", &CaptchaType::CloudflareTurnstile), Some(SolveMethod::BehavioralBypass));
47//! ```
48//!
49//! # Example — frame evaluation
50//!
51//! ```rust,no_run
52//! use captchaforge::frame::evaluate_in_all_frames;
53//! # async fn run(page: &chromiumoxide::Page) -> anyhow::Result<()> {
54//! let titles: Vec<String> = evaluate_in_all_frames(page, "document.title").await?;
55//! # Ok(()) }
56//! ```
57//!
58//! # Example — behavior simulation
59//!
60//! ```rust,no_run
61//! use captchaforge::behavior::{mouse_move_bezier, click_realistic};
62//! # async fn run(page: &chromiumoxide::Page) -> anyhow::Result<()> {
63//! mouse_move_bezier(page, 0.0, 0.0, 400.0, 300.0).await?;
64//! click_realistic(page, 400.0, 300.0).await?;
65//! # Ok(()) }
66//! ```
67
68#![forbid(unsafe_code)]
69
70pub mod behavior;
71pub mod config;
72pub mod cookies;
73pub mod detect;
74pub mod frame;
75pub mod provider;
76pub mod solver;
77pub mod stealth;
78pub mod stealth_profiles;
79pub mod telemetry;
80pub mod warmup;
81
82pub use config::Config;
83pub use cookies::CapturedCookie;
84
85// Back-compat re-export so code that used `golemn_browser::captcha_detect`
86// can `use captchaforge::captcha_detect` instead.
87pub use detect as captcha_detect;
88
89pub use provider::{CaptchaProvider, ProviderRegistry};
90
91/// One-call convenience: detect + solve in a single API.
92///
93/// Equivalent to:
94///
95/// ```ignore
96/// let info = captchaforge::detect::detect(page).await?;
97/// if !captchaforge::detect::is_captcha(&info) {
98/// return Ok(None);
99/// }
100/// let chain = captchaforge::solver::CaptchaSolverChain::default_chain();
101/// Ok(Some(chain.solve(page, &info).await))
102/// ```
103///
104/// Returns:
105/// * `Ok(None)` — no captcha was detected on the page.
106/// * `Ok(Some(result))` — captcha detected; `result.success` indicates
107/// whether the chain solved it. Failed solves still return Some
108/// so the caller can inspect the screenshot for human fallback.
109/// * `Err(_)` — the detection step itself failed (page eval error,
110/// CDP transport error). Solver errors are swallowed into a
111/// no-op `CaptchaSolveResult` and don't surface here.
112///
113/// Power users that need a custom chain (telemetry, token cache,
114/// pattern store, custom solvers) construct their own
115/// [`solver::CaptchaSolverChain`] and call `chain.solve()` directly.
116pub async fn auto_solve(
117 page: &chromiumoxide::Page,
118) -> anyhow::Result<Option<solver::CaptchaSolveResult>> {
119 // Best-effort stealth — overrides navigator.webdriver, fakes
120 // plugins/languages/WebGL fingerprint, etc. Errors are non-fatal:
121 // some chromium builds reject `addScriptToEvaluateOnNewDocument`
122 // mid-session, and we'd rather still try to solve than refuse.
123 // Production deployments should call `stealth::apply_stealth(page)`
124 // themselves immediately after creating the page (BEFORE the
125 // first goto) — that catches first-page detection probes too.
126 if let Err(e) = stealth::apply_stealth(page).await {
127 tracing::debug!("stealth::apply_stealth failed (continuing): {e}");
128 }
129 let info = detect::detect(page).await?;
130 if !detect::is_captcha(&info) {
131 return Ok(None);
132 }
133 // Discover Tier-A config (.captchaforge.toml etc) and build the
134 // chain from it — so a user's TOML overrides for cache TTL, VLM
135 // endpoint, third-party service all reach this one-call path.
136 // Discovery returns Config::default() when no file is present, so
137 // a no-config install gets the same chain shape as before.
138 // Config::build_chain() also installs the bundled provider registry.
139 let cfg = config::Config::discover()?;
140 let chain = cfg.build_chain()?;
141 Ok(Some(chain.solve(page, &info).await))
142}