captchaforge 0.1.0

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers (Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio, sliders).
Documentation
/// Human behavior simulation — realistic mouse, keyboard, and scroll patterns.
///
/// Anti-bot detectors analyze:
///   - Mouse trajectory (bots move in straight lines)
///   - Typing cadence (bots type at constant speed)
///   - Scroll patterns (bots scroll uniformly)
///   - Idle time (bots never pause)
///
/// This module generates Bézier-curve mouse paths, variable typing delays,
/// and natural scroll increments. All async fns use SmallRng (Send-safe) so
/// futures remain Send-compatible for multi-threaded Tokio runtimes.
use chromiumoxide::cdp::browser_protocol::input::{
    DispatchKeyEventParams, DispatchKeyEventType, DispatchMouseEventParams, DispatchMouseEventType,
    MouseButton,
};
use chromiumoxide::Page;
use rand::{Rng, SeedableRng};
use std::time::Duration;

/// Move mouse along a Bézier curve from (x0,y0) to (x1,y1).
/// Generates 8-20 intermediate points with realistic timing.
pub async fn mouse_move_bezier(
    page: &Page,
    x0: f64,
    y0: f64,
    x1: f64,
    y1: f64,
) -> anyhow::Result<()> {
    let mut rng = rand::rngs::StdRng::from_entropy();
    let steps = rng.gen_range(8..=20);

    // Two random control points for the cubic Bézier
    let cx1 = x0 + (x1 - x0) * rng.gen_range(0.2..0.5) + rng.gen_range(-30.0..30.0);
    let cy1 = y0 + (y1 - y0) * rng.gen_range(0.1..0.4) + rng.gen_range(-20.0..20.0);
    let cx2 = x0 + (x1 - x0) * rng.gen_range(0.6..0.9) + rng.gen_range(-20.0..20.0);
    let cy2 = y0 + (y1 - y0) * rng.gen_range(0.5..0.8) + rng.gen_range(-15.0..15.0);

    for i in 0..=steps {
        let t = i as f64 / steps as f64;
        let u = 1.0 - t;

        // Cubic Bézier: B(t) = (1-t)³P0 + 3(1-t)²tP1 + 3(1-t)t²P2 + t³P3
        let x = u * u * u * x0 + 3.0 * u * u * t * cx1 + 3.0 * u * t * t * cx2 + t * t * t * x1;
        let y = u * u * u * y0 + 3.0 * u * u * t * cy1 + 3.0 * u * t * t * cy2 + t * t * t * y1;

        let params = DispatchMouseEventParams::builder()
            .r#type(DispatchMouseEventType::MouseMoved)
            .x(x)
            .y(y)
            .build()
            .map_err(anyhow::Error::msg)?;
        page.execute(params).await?;

        // Variable delay: faster in middle, slower at start/end (ease-in-out)
        let ease = (std::f64::consts::PI * t).sin(); // 0 at endpoints, 1 at midpoint
        let base_ms = rng.gen_range(8..25);
        let delay_ms = base_ms + ((1.0 - ease) * 15.0) as u64;
        tokio::time::sleep(Duration::from_millis(delay_ms)).await;
    }

    Ok(())
}

/// Click at position with realistic mouse-down delay.
pub async fn click_realistic(page: &Page, x: f64, y: f64) -> anyhow::Result<()> {
    let mut rng = rand::rngs::StdRng::from_entropy();

    // Mouse down
    let down = DispatchMouseEventParams::builder()
        .r#type(DispatchMouseEventType::MousePressed)
        .x(x)
        .y(y)
        .button(MouseButton::Left)
        .click_count(1)
        .build()
        .map_err(anyhow::Error::msg)?;
    page.execute(down).await?;

    // Realistic hold time: 50-150ms
    tokio::time::sleep(Duration::from_millis(rng.gen_range(50..150))).await;

    // Mouse up
    let up = DispatchMouseEventParams::builder()
        .r#type(DispatchMouseEventType::MouseReleased)
        .x(x)
        .y(y)
        .button(MouseButton::Left)
        .click_count(1)
        .build()
        .map_err(anyhow::Error::msg)?;
    page.execute(up).await?;

    Ok(())
}

/// Type text with human-like cadence: variable inter-key delays, occasional pauses.
/// Average typing speed: ~200ms per character (60 WPM) with variance.
pub async fn type_realistic(page: &Page, text: &str) -> anyhow::Result<()> {
    let mut rng = rand::rngs::StdRng::from_entropy();

    for (i, ch) in text.chars().enumerate() {
        let key_text = ch.to_string();

        // Key down
        let down = DispatchKeyEventParams::builder()
            .r#type(DispatchKeyEventType::KeyDown)
            .text(&key_text)
            .build()
            .map_err(anyhow::Error::msg)?;
        page.execute(down).await?;

        // Key press delay: 30-80ms
        tokio::time::sleep(Duration::from_millis(rng.gen_range(30..80))).await;

        // Key up
        let up = DispatchKeyEventParams::builder()
            .r#type(DispatchKeyEventType::KeyUp)
            .text(&key_text)
            .build()
            .map_err(anyhow::Error::msg)?;
        page.execute(up).await?;

        // Inter-key delay: 80-250ms with occasional longer pauses
        let base = rng.gen_range(80..250);
        let pause = if i > 0 && i % rng.gen_range(4..10) == 0 {
            rng.gen_range(200..600) // "thinking" pause every few characters
        } else {
            0
        };
        tokio::time::sleep(Duration::from_millis(base + pause)).await;
    }

    Ok(())
}

/// Scroll with human-like pattern: variable speed, direction micro-corrections.
pub async fn scroll_realistic(
    page: &Page,
    direction: ScrollDirection,
    amount: u32,
) -> anyhow::Result<()> {
    let mut rng = rand::rngs::StdRng::from_entropy();
    let steps = rng.gen_range(3..=8).min(amount);
    let per_step = (amount as f64 / steps as f64) as i32;

    for _ in 0..steps {
        let delta = match direction {
            ScrollDirection::Down => per_step + rng.gen_range(-20..20),
            ScrollDirection::Up => -(per_step + rng.gen_range(-20..20)),
        };

        let js = format!("window.scrollBy(0, {})", delta);
        page.evaluate(js).await?;

        // Variable scroll delay: 100-400ms
        tokio::time::sleep(Duration::from_millis(rng.gen_range(100..400))).await;
    }

    Ok(())
}

/// Random idle pause — simulates reading/thinking time.
pub async fn idle_pause() {
    let ms = rand::rngs::StdRng::from_entropy().gen_range(500..2000);
    tokio::time::sleep(Duration::from_millis(ms)).await;
}

/// Short micro-pause between actions (100-400ms).
pub async fn micro_pause() {
    let ms = rand::rngs::StdRng::from_entropy().gen_range(100..400);
    tokio::time::sleep(Duration::from_millis(ms)).await;
}

#[derive(Debug, Clone, Copy)]
pub enum ScrollDirection {
    Up,
    Down,
}

#[cfg(test)]
mod tests {
    #[test]
    fn bezier_control_points_within_range() {
        // Verify the Bézier math produces values between start and end (approximately)
        let (x0, _y0) = (100.0, 200.0);
        let (x1, _y1) = (500.0, 400.0);

        // Just verify the formula doesn't panic at t=0 and t=1
        let t0 = 0.0_f64;
        let t1 = 1.0_f64;

        let u0 = 1.0 - t0;
        let u1 = 1.0 - t1;

        let bx0 = u0.powi(3) * x0 + t0.powi(3) * x1;
        let bx1 = u1.powi(3) * x0 + t1.powi(3) * x1;

        assert!((bx0 - x0).abs() < 0.001);
        assert!((bx1 - x1).abs() < 0.001);
    }
}