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
//! Canvas / SVG / math / icon / color CAPTCHA detector.
//!
//! Targets in-house captcha widgets that render to a `<canvas>` or
//! `<svg>` element rather than using a SaaS provider.
use crate::Page;
use anyhow::Result;
use async_trait::async_trait;

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

/// Detector for canvas/SVG/math/icon/color CAPTCHAs.
pub struct CanvasCaptchaDetector;

impl CanvasCaptchaDetector {
    /// JS payload evaluated against the live page.
    pub const JS: &'static str = r#"(function() {
    const canvas = document.querySelector(
        'canvas.captcha, #captcha canvas, ' +
        '[class*="canvas-captcha"], [class*="svg-captcha"], ' +
        '[class*="math-captcha"], [class*="icon-captcha"], ' +
        '[class*="color-captcha"], [class*="rotated-text"], ' +
        // Generic "image-as-challenge" wrappers: a captcha-image
        // class containing a visual + an answer input.
        '.captcha-img, .captcha-image, ' +
        // Pickers (icon / color) and homegrown captcha boxes by ID.
        '.icon-grid, .color-grid, ' +
        '[id*="captcha-box"], [id*="captcha-input"], ' +
        // Game-style captchas (kill enemies, click targets, etc.).
        '[class*="game-captcha"], [class*="doom-captcha"]'
    );
    if (canvas) {
        return { kind: 'canvas', site_key: null, container: 'canvas.captcha' };
    }
    return { kind: 'none', site_key: null, container: null };
})()"#;
}

#[async_trait]
impl Detector for CanvasCaptchaDetector {
    fn name(&self) -> &'static str {
        "canvas_captcha"
    }
    fn priority(&self) -> i32 {
        65
    }
    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 canvas_captcha_detector_js_has_selectors() {
        assert!(CanvasCaptchaDetector::JS.contains("canvas.captcha"));
        assert!(CanvasCaptchaDetector::JS.contains("#captcha canvas"));
        assert!(CanvasCaptchaDetector::JS.contains("[class*=\"canvas-captcha\"]"));
        assert!(CanvasCaptchaDetector::JS.contains("[class*=\"svg-captcha\"]"));
        assert!(CanvasCaptchaDetector::JS.contains("[class*=\"math-captcha\"]"));
        assert!(CanvasCaptchaDetector::JS.contains("[class*=\"icon-captcha\"]"));
        assert!(CanvasCaptchaDetector::JS.contains("[class*=\"color-captcha\"]"));
        assert!(CanvasCaptchaDetector::JS.contains("[class*=\"rotated-text\"]"));
    }

    #[test]
    fn canvas_captcha_detector_js_has_extended_selectors() {
        // Extension markers added to catch the long tail of in-house
        // captcha widgets: image-as-challenge wrappers, icon/color
        // pickers, swipe-to-verify bars.
        assert!(CanvasCaptchaDetector::JS.contains(".captcha-img"));
        assert!(CanvasCaptchaDetector::JS.contains(".captcha-image"));
        assert!(CanvasCaptchaDetector::JS.contains(".icon-grid"));
        assert!(CanvasCaptchaDetector::JS.contains(".color-grid"));
        assert!(CanvasCaptchaDetector::JS.contains("[id*=\"captcha-box\"]"));
        assert!(CanvasCaptchaDetector::JS.contains("[id*=\"captcha-input\"]"));
    }
}