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
//! Cloudflare Turnstile detector.
//!
//! Two probes: the `.cf-turnstile` container with `data-sitekey`, and a
//! script tag pointing at `challenges.cloudflare.com` plus a generic
//! `[data-sitekey]` element that no other provider has claimed.
use crate::Page;
use anyhow::Result;
use async_trait::async_trait;

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

/// Detector for Cloudflare Turnstile widgets.
pub struct TurnstileDetector;

impl TurnstileDetector {
    /// JS payload evaluated against the live page.
    pub const JS: &'static str = r#"(function() {
    const turnstile = document.querySelector('.cf-turnstile, [data-turnstile-sitekey]');
    if (turnstile) {
        return {
            kind: 'turnstile',
            site_key: turnstile.getAttribute('data-sitekey') ||
                      turnstile.getAttribute('data-turnstile-sitekey'),
            container: '.cf-turnstile'
        };
    }
    const turnstileScript = document.querySelector('script[src*="challenges.cloudflare.com"]');
    if (turnstileScript) {
        // Explicit-render mounts (e.g. turnstile.render('#cf-turnstile-mount', ...))
        // may not have data-sitekey on a DOM element. Accept the script presence
        // as sufficient evidence when paired with a known mount point.
        const mount = document.querySelector('#cf-turnstile, #cf-turnstile-mount, [id*="turnstile"]');
        const cfEl = document.querySelector('[data-sitekey]');
        if (mount || cfEl) {
            return {
                kind: 'turnstile',
                site_key: cfEl ? cfEl.getAttribute('data-sitekey') : null,
                container: mount ? ('#' + mount.id) : (cfEl ? '[data-sitekey]' : null)
            };
        }
        // Fallback: script alone is strong evidence of Turnstile
        return { kind: 'turnstile', site_key: null, container: null };
    }
    return { kind: 'none', site_key: null, container: null };
})()"#;
}

#[async_trait]
impl Detector for TurnstileDetector {
    fn name(&self) -> &'static str {
        "turnstile"
    }
    fn priority(&self) -> i32 {
        10
    }
    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 turnstile_detector_js_has_selectors() {
        assert!(TurnstileDetector::JS.contains(".cf-turnstile"));
        assert!(TurnstileDetector::JS.contains("[data-turnstile-sitekey]"));
        assert!(TurnstileDetector::JS.contains("challenges.cloudflare.com"));
        assert!(TurnstileDetector::JS.contains("#cf-turnstile"));
        assert!(TurnstileDetector::JS.contains("#cf-turnstile-mount"));
        assert!(TurnstileDetector::JS.contains("turnstile"));
    }
}