captchaforge 0.2.39

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
//! Max-coherence contract: the default `apply_stealth` (real identity +
//! automation-tell scrubbing) must keep the JS-visible identity IN SYNC with the
//! real HTTP request. A spoofed `navigator.userAgent` that disagrees with the
//! `User-Agent` request header is a self-inflicted tell every serious anti-bot
//! cross-checks; the genuine-identity default must never produce that.
//!
//! Live: requires Firefox (via `common::launch_browser`).

use std::io::{Read, Write};
use std::net::TcpListener;
use std::sync::mpsc;

mod common;

/// Serve one request, capture its `User-Agent` header, and reply with a page
/// that echoes nothing (the JS UA is read separately). Returns the captured
/// header via the channel.
fn serve_capturing_ua(listener: TcpListener, tx: mpsc::Sender<String>) {
    std::thread::spawn(move || {
        if let Some(Ok(mut s)) = listener.incoming().next() {
            let mut buf = [0u8; 4096];
            let n = s.read(&mut buf).unwrap_or(0);
            let req = String::from_utf8_lossy(&buf[..n]);
            let ua = req
                .lines()
                .find(|l| l.to_ascii_lowercase().starts_with("user-agent:"))
                .map(|l| l[l.find(':').unwrap() + 1..].trim().to_string())
                .unwrap_or_default();
            let _ = tx.send(ua);
            let body = "<!doctype html><html><body>ok</body></html>";
            let resp = format!(
                "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
                body.len(),
                body
            );
            let _ = s.write_all(resp.as_bytes());
            let _ = s.flush();
        }
    });
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn apply_stealth_keeps_ua_coherent_and_hides_webdriver() {
    let listener = TcpListener::bind("127.0.0.1:0").unwrap();
    let addr = listener.local_addr().unwrap();
    let (tx, rx) = mpsc::channel();
    serve_capturing_ua(listener, tx);

    let browser = common::launch_browser().await;
    let page = browser.new_page("about:blank").await.unwrap();

    // Default max-coherence disguise: scrub automation tells, keep real identity.
    captchaforge::apply_stealth(page).await.unwrap();

    page.goto(&format!("http://{addr}/"))
        .await
        .expect("navigate");

    let http_ua = rx
        .recv_timeout(std::time::Duration::from_secs(10))
        .expect("server captured a request User-Agent");
    let js_ua: String = page
        .evaluate("navigator.userAgent")
        .await
        .unwrap()
        .into_value()
        .unwrap();

    eprintln!("[coherence] HTTP UA: {http_ua}");
    eprintln!("[coherence] JS   UA: {js_ua}");

    assert!(!http_ua.is_empty(), "server must see a User-Agent header");
    assert!(
        js_ua.contains("Firefox/"),
        "real Firefox UA expected, got {js_ua}"
    );
    assert_eq!(
        http_ua, js_ua,
        "max-coherence default must keep the HTTP User-Agent header IN SYNC with \
         navigator.userAgent (no spoof-induced desync)"
    );

    // Automation tell must still be scrubbed even though identity is genuine.
    let webdriver_ok: bool = page
        .evaluate("navigator.webdriver === false || navigator.webdriver === undefined")
        .await
        .unwrap()
        .into_value()
        .unwrap();
    assert!(webdriver_ok, "navigator.webdriver must be scrubbed");

    browser.close().await.unwrap();
}