captchaforge 0.2.26

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers (Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio, sliders).
Documentation
//! Generic image CAPTCHA detector.
//!
//! Looks for `<img>` elements whose `src` contains "captcha" (any
//! capitalisation) and for `<img>` children of typical container
//! elements (`.captcha`, `#captcha`).
use anyhow::Result;
use async_trait::async_trait;
use chromiumoxide::Page;

use super::{parse_detection_result, CaptchaInfo, Detector};

/// Detector for generic image CAPTCHAs.
pub struct ImageCaptchaDetector;

impl ImageCaptchaDetector {
    /// JS payload evaluated against the live page.
    pub const JS: &'static str = r#"(function() {
    const imgCaptcha = document.querySelector(
        'img[src*="captcha"], img[src*="Captcha"], .captcha img, #captcha img'
    );
    if (imgCaptcha) {
        return { kind: 'image', site_key: null, container: '.captcha, #captcha' };
    }
    /* Tile-grid widgets (icon picker, color picker, image grid). The
       VLM grid-click branch needs Image kind, not Canvas. We
       disambiguate by requiring 4+ tile children — the giveaway for a
       grid challenge vs a single-image captcha. */
    const gridSelectors = [
        '.icon-grid', '.color-grid', '.captcha-grid',
        '[class*="icon-captcha-grid"]', '[class*="image-grid"]'
    ];
    for (const sel of gridSelectors) {
        const grid = document.querySelector(sel);
        if (grid && grid.children && grid.children.length >= 4) {
            return { kind: 'image', site_key: null, container: sel };
        }
    }
    return { kind: 'none', site_key: null, container: null };
})()"#;
}

#[async_trait]
impl Detector for ImageCaptchaDetector {
    fn name(&self) -> &'static str {
        "image_captcha"
    }
    fn priority(&self) -> i32 {
        40
    }
    async fn detect(&self, page: &Page) -> Result<Option<CaptchaInfo>> {
        let raw = page.evaluate(Self::JS).await?;
        let val = raw.into_value::<serde_json::Value>()?;
        Ok(parse_detection_result(val))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn image_captcha_detector_js_has_selectors() {
        assert!(ImageCaptchaDetector::JS.contains("img[src*=\"captcha\"]"));
        assert!(ImageCaptchaDetector::JS.contains(".captcha img"));
    }

    #[test]
    fn image_captcha_detector_js_handles_tile_grids() {
        // Grid-pick widgets (icon / color / image-grid) need to route
        // to the VLM grid-click branch. We require 4+ tiles to
        // disambiguate from single-image captchas.
        let js = ImageCaptchaDetector::JS;
        assert!(js.contains(".icon-grid"));
        assert!(js.contains(".color-grid"));
        assert!(js.contains(".captcha-grid"));
        assert!(js.contains("children.length >= 4"));
    }
}