captchaforge 0.2.36

[DO NOT USE — UNDER ACTIVE DEVELOPMENT, NOT PRODUCTION-READY] Captcha solver scaffolding for chromiumoxide-driven browsers. The architecture is in place (vendor solvers, retry-loop iframe walking, VLM provider abstraction, real-WAF bench harness) but the live-vendor success rate is still 0% — Cloudflare Turnstile / hCaptcha / reCAPTCHA detect us at a TLS / CDP fingerprint layer that no flag-based stealth has cleared. Watch the repo; do not depend on this for any real workload.
Documentation
//! `cargo run --example solve_url_simple -- <https-url>`
//!
//! The simplest possible captchaforge usage:
//!
//! 1. Launch a chromium browser via [`chromiumoxide`].
//! 2. Hand a fresh page + URL to [`captchaforge::solve_url`].
//! 3. Print the result.
//!
//! `solve_url` wires `prepare_page` (stealth + named profile + warmup)
//! BEFORE the navigation, then `auto_solve` AFTER. Three lines of
//! captchaforge in total. Pass `--profile <name>` (e.g.
//! `--profile chrome-mac`) to pin a named [`StealthProfile`].

use anyhow::{Context, Result};
use captchaforge::{solve_url, StealthProfile};
use chromiumoxide::{Browser, BrowserConfig};
use futures_util::stream::StreamExt;

#[tokio::main]
async fn main() -> Result<()> {
    let mut args = std::env::args().skip(1);
    let url = args
        .next()
        .context("usage: solve_url_simple <url> [--profile <name>]")?;
    let profile = parse_profile_arg(args)?;

    let (mut browser, mut handler) = Browser::launch(
        BrowserConfig::builder()
            .build()
            .map_err(|e| anyhow::anyhow!("BrowserConfig::build: {e}"))?,
    )
    .await
    .context("launching chromium")?;

    // Drive the CDP event loop in the background — chromiumoxide
    // requires this; without it `page.goto` etc. hang.
    let handler_task = tokio::spawn(async move {
        while let Some(_event) = handler.next().await {
            // Drop CDP events; we don't need to react to them.
        }
    });

    let page = browser.new_page("about:blank").await.context("new_page")?;
    let outcome = solve_url(&page, &url, profile).await?;

    match outcome {
        None => println!("no captcha detected at {url}"),
        Some(r) => println!(
            "captcha {} via {:?} in {}ms (confidence={:.2}, verified={:?})",
            if r.success { "SOLVED" } else { "FAILED" },
            r.method,
            r.time_ms,
            r.confidence,
            r.verified_outcome,
        ),
    }

    // Polite shutdown so the chromium child exits cleanly.
    browser.close().await.ok();
    handler_task.abort();
    Ok(())
}

/// Parse `--profile <name>` out of the remaining CLI args. Returns
/// `Ok(None)` when the flag is absent.
fn parse_profile_arg(mut args: impl Iterator<Item = String>) -> Result<Option<StealthProfile>> {
    while let Some(a) = args.next() {
        if a == "--profile" {
            let name = args.next().context("--profile requires a value")?;
            return Ok(Some(profile_from_name(&name)?));
        }
    }
    Ok(None)
}

fn profile_from_name(name: &str) -> Result<StealthProfile> {
    Ok(match name.to_ascii_lowercase().as_str() {
        "chrome-windows" | "chrome-win" => StealthProfile::ChromeWindowsStable,
        "chrome-mac" | "chrome-osx" => StealthProfile::ChromeMacStable,
        "chrome-linux" => StealthProfile::ChromeLinux,
        "chrome-android" | "android" => StealthProfile::ChromeAndroid,
        "edge-windows" | "edge" => StealthProfile::EdgeWindowsStable,
        "firefox-linux" | "firefox" => StealthProfile::FirefoxLinux,
        "safari-iphone" | "iphone" => StealthProfile::SafariIphone,
        "safari-ipad" | "ipad" => StealthProfile::SafariIpad,
        "safari-mac" | "safari" => StealthProfile::SafariMacStable,
        "brave-windows" | "brave" => StealthProfile::BraveWindows,
        "opera-windows" | "opera" => StealthProfile::OperaWindows,
        "samsung-internet" | "samsung" => StealthProfile::SamsungInternetAndroid,
        other => anyhow::bail!(
            "unknown stealth profile {other:?}; try `captchaforge profiles` for the full list"
        ),
    })
}