captchaforge 0.2.6

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers (Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio, sliders).
Documentation
use super::CaptchaType;
use base64::Engine as _;

// ─── Utilities ───────────────────────────────────────────────────────────────

pub fn extract_domain(page_url: &str) -> String {
    // 1. Try proper URL parsing (handles https://example.com:8080/path)
    if let Ok(u) = url::Url::parse(page_url) {
        if let Some(host) = u.host_str() {
            return host.to_string();
        }
    }
    // 2. Fallback for strings that contain "://" but aren't valid URLs
    //    (e.g. malformed URLs that url::Url rejects)
    if let Some(after_scheme) = page_url.split("://").nth(1) {
        return after_scheme
            .split('/')
            .next()
            .unwrap_or("")
            .split(':')
            .next()
            .unwrap_or("")
            .to_string();
    }
    // 3. Bare hostname like "example.com" — strip any path/port manually.
    //    Reject inputs that don't look like hostnames (contain spaces, etc.)
    let candidate = page_url
        .split('/')
        .next()
        .unwrap_or("")
        .split(':')
        .next()
        .unwrap_or("")
        .trim();
    if candidate.is_empty() || candidate.contains(' ') {
        String::new()
    } else {
        candidate.to_string()
    }
}

pub fn detected_to_type(detected: &crate::captcha_detect::DetectedCaptcha) -> CaptchaType {
    use crate::captcha_detect::DetectedCaptcha;
    match detected {
        DetectedCaptcha::RecaptchaV2 => CaptchaType::RecaptchaV2,
        DetectedCaptcha::RecaptchaV3 => CaptchaType::RecaptchaV3,
        DetectedCaptcha::HCaptcha => CaptchaType::HCaptcha,
        DetectedCaptcha::Turnstile => CaptchaType::CloudflareTurnstile,
        DetectedCaptcha::ImageCaptcha => CaptchaType::ImageGrid,
        DetectedCaptcha::AudioCaptcha => CaptchaType::AudioCaptcha,
        DetectedCaptcha::PowCaptcha => CaptchaType::PowCaptcha,
        DetectedCaptcha::SliderCaptcha => CaptchaType::Slider,
        DetectedCaptcha::CanvasCaptcha => CaptchaType::CanvasCaptcha,
        DetectedCaptcha::ShadowDomCaptcha => CaptchaType::ShadowDomCaptcha,
        DetectedCaptcha::MultiStepCaptcha => CaptchaType::MultiStepCaptcha,
        // TOML-rule providers map to a Custom CaptchaType named after
        // the rule, so the solver chain + pattern store key on the
        // vendor identifier (`aws_waf`, `datadome`, etc.).
        DetectedCaptcha::Custom(name) => CaptchaType::Custom(name.clone()),
        DetectedCaptcha::None => CaptchaType::Custom("none".to_string()),
    }
}

/// Take a JPEG screenshot and return it as a standard base64 string.
pub(crate) async fn screenshot_b64(page: &chromiumoxide::Page) -> anyhow::Result<String> {
    let data = page
        .screenshot(
            chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotParams::builder()
                .format(chromiumoxide::cdp::browser_protocol::page::CaptureScreenshotFormat::Jpeg)
                .quality(85)
                .build(),
        )
        .await?;
    Ok(base64::engine::general_purpose::STANDARD.encode(&data))
}