captchaforge 0.2.40

Captcha detection and solving for Firefox and BiDi-driven browsers. Detection, vendor solver scaffolding, trusted cross-origin click delivery into nested OOPIFs, and stealth personas are implemented and tested; broad live-vendor solve rates are not yet benchmarked.
Documentation
//! End-to-end proof that the reynard ENGINE closes the `navigator.webdriver`
//! tell that plain BiDi Firefox cannot.
//!
//! Companion to `webdriver_native_pref.rs` (which proves the pref is overridden
//! on plain Firefox so native `navigator.webdriver` stays `true`). reynard pins
//! it `false` in the Gecko C++; this drives the real engine and reads the value.
//!
//! Headless. Opt-in via `REYNARD_BIN` (point at a CURRENT-Firefox build (e.g).
//! `…/reynard-staging/camoufox-150…/obj-*/dist/bin/camoufox`; the old Fx133
//! `~/.cache/camoufox` build can't attach to rustenium 1.1.10). Skips cleanly
//! otherwise. `MOZ_DISABLE_CONTENT_SANDBOX=1` recommended.

use captchaforge::stealth::browser::launch_reynard;
use captchaforge::StealthProfile;
use std::time::Duration;

fn reynard_binary() -> Option<String> {
    if let Ok(p) = std::env::var("REYNARD_BIN") {
        if !p.trim().is_empty() {
            return Some(p);
        }
    }
    let home = std::env::var("HOME").ok()?;
    let cached = format!("{home}/.cache/camoufox/camoufox");
    std::path::Path::new(&cached).exists().then_some(cached)
}

async fn within<T>(secs: u64, fut: impl std::future::Future<Output = T>) -> Option<T> {
    tokio::time::timeout(Duration::from_secs(secs), fut)
        .await
        .ok()
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn reynard_engine_closes_native_webdriver() {
    let Some(bin) = reynard_binary() else {
        eprintln!("SKIP reynard_webdriver: no REYNARD_BIN and no ~/.cache/camoufox/camoufox");
        return;
    };
    eprintln!("[reynard] binary: {bin}");

    let page = match within(
        90,
        launch_reynard(&bin, &StealthProfile::FirefoxLinux, true),
    )
    .await
    {
        Some(Ok(p)) => p,
        Some(Err(e)) => {
            eprintln!("SKIP reynard_webdriver: launch_reynard failed ({e})");
            return;
        }
        None => {
            eprintln!("SKIP reynard_webdriver: launch_reynard timed out (90s)");
            return;
        }
    };
    eprintln!("[reynard] launched + attached over BiDi");

    if within(25, page.goto("about:blank")).await.is_none() {
        eprintln!("SKIP reynard_webdriver: goto timed out");
        let _ = within(5, page.close()).await;
        return;
    }
    let native = match within(25, page.evaluate("String(navigator.webdriver)")).await {
        Some(Ok(ev)) => ev
            .into_value::<String>()
            .unwrap_or_else(|_| "<parse-fail>".into()),
        _ => {
            eprintln!("SKIP reynard_webdriver: evaluate timed out / errored");
            let _ = within(5, page.close()).await;
            return;
        }
    };
    eprintln!("[reynard] native navigator.webdriver = {native}");
    let _ = within(8, page.close()).await;

    assert_eq!(
        native, "false",
        "the reynard engine must read native navigator.webdriver=false even under BiDi \
The webDriverAdvanced tell plain Firefox cannot hide."
    );
}