captchaforge 0.2.35

[DO NOT USE — UNDER ACTIVE DEVELOPMENT, NOT PRODUCTION-READY] Captcha solver scaffolding for chromiumoxide-driven browsers. The architecture is in place (vendor solvers, retry-loop iframe walking, VLM provider abstraction, real-WAF bench harness) but the live-vendor success rate is still 0% — Cloudflare Turnstile / hCaptcha / reCAPTCHA detect us at a TLS / CDP fingerprint layer that no flag-based stealth has cleared. Watch the repo; do not depend on this for any real workload.
Documentation
//! Generic slider / swipe / rotate / drag CAPTCHA detector.
//!
//! Coverage is intentionally broad to catch the long tail of in-house
//! slider widgets ("class contains 'slider', 'swipe', 'rotate',
//! 'motion'"). Specific vendors with stable signatures (Geetest v3/v4
//! etc.) should ship their own dedicated detector at a lower priority
//! value so they win over this generic one.
use anyhow::Result;
use async_trait::async_trait;
use chromiumoxide::Page;

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

/// Detector for generic slider-style CAPTCHAs.
pub struct SliderCaptchaDetector;

impl SliderCaptchaDetector {
    /// JS payload evaluated against the live page.
    pub const JS: &'static str = r#"(function() {
    /* Specific class names + targeted attribute-contains selectors.
       A bare contains-selector for the bare word `rotate` would also
       match `rotated-text` (a static-text captcha — wrong solver),
       so we require the `-captcha` suffix throughout. ROOT classes:
       `slider-captcha`, `swipe-captcha`, `rotate-captcha`,
       `motion-captcha` plus the bare ID `#slider`. */
    const slider = document.querySelector(
        '.slider-captcha, #slider, ' +
        '.swipe-captcha, .rotate-captcha, .motion-captcha, ' +
        '[class*="slider-captcha"], [class*="swipe-captcha"], ' +
        '[class*="rotate-captcha"], [class*="motion-captcha"]'
    );
    if (slider) {
        return { kind: 'slider', site_key: null, container: '.slider-captcha' };
    }
    const rangeInCaptcha = document.querySelector('.captcha input[type="range"], #captcha input[type="range"], [class*="captcha"] input[type="range"]');
    if (rangeInCaptcha) {
        return { kind: 'slider', site_key: null, container: '.captcha input[type="range"]' };
    }
    const canvasDrag = document.querySelector('canvas[class*="drag"], canvas[class*="slider"], canvas[class*="swipe"]');
    if (canvasDrag) {
        return { kind: 'slider', site_key: null, container: 'canvas[class*="drag"]' };
    }
    return { kind: 'none', site_key: null, container: null };
})()"#;
}

#[async_trait]
impl Detector for SliderCaptchaDetector {
    fn name(&self) -> &'static str {
        "slider_captcha"
    }
    fn priority(&self) -> i32 {
        // Lower than canvas_captcha (65) because slider widgets often
        // contain a captcha-classed `<canvas>` for the puzzle visual —
        // we need to claim the kind as SliderCaptcha so the slider
        // solver runs, not the VLM canvas-text branch.
        60
    }
    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 slider_captcha_detector_js_has_selectors() {
        let js = SliderCaptchaDetector::JS;
        assert!(js.contains(".slider-captcha"));
        assert!(js.contains("#slider"));
        assert!(js.contains(".swipe-captcha"));
        assert!(js.contains(".rotate-captcha"));
        assert!(js.contains(".motion-captcha"));
        assert!(js.contains("input[type=\"range\"]"));
        assert!(js.contains("canvas[class*=\"drag\"]"));
    }

    #[test]
    fn slider_captcha_detector_does_not_overmatch_rotated_text() {
        // Pin: bare `[class*="rotate"]` (closing-bracket immediately
        // after) would catch the static `rotated-text` captcha (a
        // text-read challenge, not a slider). We require the
        // `-captcha` suffix instead so static rotated-text routes to
        // the canvas/VLM path.
        let js = SliderCaptchaDetector::JS;
        assert!(
            !js.contains(r#"[class*="rotate"]"#),
            "must not match bare `rotate` substring (use `rotate-captcha` instead)"
        );
        assert!(
            !js.contains(r#"[class*="motion"]"#),
            "must not match bare `motion` substring (use `motion-captcha` instead)"
        );
        assert!(js.contains("[class*=\"rotate-captcha\"]"));
        assert!(js.contains("[class*=\"motion-captcha\"]"));
    }
}