captchaforge 0.2.7

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers (Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio, sliders).
Documentation
//! hCaptcha detector.
//!
//! Probes for `.h-captcha`/`[data-hcaptcha-sitekey]` containers and a
//! script tag pointing at `hcaptcha.com`. Falls back to a generic
//! `[data-sitekey]` element only when no other provider has claimed
//! it (`.g-recaptcha` excluded; `data-turnstile-sitekey` excluded).
use anyhow::Result;
use async_trait::async_trait;
use chromiumoxide::Page;

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

/// Detector for hCaptcha widgets.
pub struct HCaptchaDetector;

impl HCaptchaDetector {
    /// JS payload evaluated against the live page.
    pub const JS: &'static str = r#"(function() {
    const hcaptcha = document.querySelector('.h-captcha, [data-hcaptcha-sitekey]');
    const hcaptchaScript = document.querySelector('script[src*="hcaptcha.com"]');
    if (hcaptcha || hcaptchaScript) {
        let el = hcaptcha;
        if (!el) {
            const fallback = document.querySelector('[data-sitekey]');
            if (fallback && !fallback.hasAttribute('data-turnstile-sitekey') && !fallback.matches('.g-recaptcha')) {
                el = fallback;
            }
        }
        const result = { kind: 'hcaptcha', site_key: null, container: '.h-captcha' };
        if (el) {
            result.site_key = el.getAttribute('data-sitekey') ||
                              el.getAttribute('data-hcaptcha-sitekey');
        }
        return result;
    }
    return { kind: 'none', site_key: null, container: null };
})()"#;
}

#[async_trait]
impl Detector for HCaptchaDetector {
    fn name(&self) -> &'static str {
        "hcaptcha"
    }
    fn priority(&self) -> i32 {
        30
    }
    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 hcaptcha_detector_js_has_selectors() {
        assert!(HCaptchaDetector::JS.contains(".h-captcha"));
        assert!(HCaptchaDetector::JS.contains("[data-hcaptcha-sitekey]"));
        assert!(HCaptchaDetector::JS.contains("hcaptcha.com"));
    }

    #[test]
    fn hcaptcha_detector_excludes_other_provider_sitekeys() {
        assert!(HCaptchaDetector::JS.contains("!fallback.hasAttribute('data-turnstile-sitekey')"));
        assert!(HCaptchaDetector::JS.contains("!fallback.matches('.g-recaptcha')"));
    }
}