captchaforge 0.2.32

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers (Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio, sliders).
Documentation
//! Multi-step / wizard CAPTCHA detector.
//!
//! Catches forms with multiple `<fieldset>`s carrying step indicators
//! (class/id contains "step"). Heuristic; false positives possible on
//! generic multi-step forms with no captcha intent.
use anyhow::Result;
use async_trait::async_trait;
use chromiumoxide::Page;

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

/// Detector for multi-step / wizard CAPTCHAs.
pub struct MultiStepCaptchaDetector;

impl MultiStepCaptchaDetector {
    /// JS payload evaluated against the live page.
    pub const JS: &'static str = r#"(function() {
    const multiStep = document.querySelector('.multi-step-captcha, .wizard-captcha, [class*="wizard"]');
    if (multiStep) {
        return { kind: 'multi_step', site_key: null, container: '.multi-step-captcha, .wizard-captcha, [class*="wizard"]' };
    }
    const forms = document.querySelectorAll('form');
    for (const form of forms) {
        const fieldsets = form.querySelectorAll('fieldset');
        if (fieldsets.length > 1) {
            const steps = form.querySelectorAll('.step, [class*="step"], [id*="step"]');
            if (steps.length > 0) {
                return { kind: 'multi_step', site_key: null, container: 'form' };
            }
        }
    }
    /* Generic .step pattern (not in a form): 2+ `.step` siblings with
       one carrying `.active`. Common in homegrown wizard widgets. */
    const steps = document.querySelectorAll('.step, [class*="step-"]');
    if (steps.length >= 2) {
        const active = document.querySelector('.step.active, .step-active, .step.is-active');
        if (active) {
            return { kind: 'multi_step', site_key: null, container: '.step.active' };
        }
    }
    return { kind: 'none', site_key: null, container: null };
})()"#;
}

#[async_trait]
impl Detector for MultiStepCaptchaDetector {
    fn name(&self) -> &'static str {
        "multi_step_captcha"
    }
    fn priority(&self) -> i32 {
        // Below math (80) so a wizard-style captcha whose first step
        // is a math question routes to the multi-step orchestrator
        // (which can advance through subsequent steps), not the
        // single-shot math solver.
        78
    }
    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 multi_step_captcha_detector_js_has_selectors() {
        assert!(MultiStepCaptchaDetector::JS.contains(".multi-step-captcha"));
        assert!(MultiStepCaptchaDetector::JS.contains(".wizard-captcha"));
        assert!(MultiStepCaptchaDetector::JS.contains("[class*=\"wizard\"]"));
        assert!(MultiStepCaptchaDetector::JS.contains("fieldset"));
        assert!(MultiStepCaptchaDetector::JS.contains("[class*=\"step\"]"));
    }
}