browsing 0.1.5

Browser automation: navigate, click, extract, screenshot. Standalone browser control via CDP.
Documentation
//! Stealth mode — hide browser automation fingerprints from detection
//!
//! Uses CDP `Page.addScriptToEvaluateOnNewDocument` to inject anti-detection
//! JavaScript on every page load. Also adds Chrome flags to disable
//! automation indicators.

use crate::browser::cdp::CdpClient;
use crate::error::Result;
use serde_json::json;
use std::sync::Arc;

/// Stealth script injected on every new document
const STEALTH_SCRIPT: &str = r#"
(() => {
    // Remove webdriver property
    delete Object.getPrototypeOf(navigator).webdriver;

    // Override permissions query to auto-grant
    const originalQuery = window.navigator.permissions.query;
    window.navigator.permissions.query = (parameters) => (
        parameters.name === 'notifications' ||
        parameters.name === 'clipboard-read' ||
        parameters.name === 'clipboard-write'
            ? Promise.resolve({ state: 'prompt', onchange: null })
            : originalQuery(parameters)
    );

    // Override plugins to look realistic
    Object.defineProperty(navigator, 'plugins', {
        get: () => [
            { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format', version: undefined, length: 1, item: (idx) => [][idx], namedItem: (name) => undefined },
            { name: 'Native Client', filename: 'internal-nacl-plugin', description: 'Native Client module', version: undefined, length: 1, item: (idx) => [][idx], namedItem: (name) => undefined },
            { name: 'Widevine Content Decryption Module', filename: 'widevinecdmadapter.dll', description: 'Widevine Content Decryption Module', version: undefined, length: 1, item: (idx) => [][idx], namedItem: (name) => undefined },
        ],
    });

    // Override mimeTypes
    Object.defineProperty(navigator, 'mimeTypes', {
        get: () => [
            { type: 'application/pdf', suffixes: 'pdf', description: 'Portable Document Format', enabledPlugin: navigator.plugins[0] },
            { type: 'application/x-google-chrome-pdf', suffixes: 'pdf', description: 'Portable Document Format', enabledPlugin: navigator.plugins[0] },
            { type: 'application/x-nacl', suffixes: '', description: 'Native Client executable', enabledPlugin: navigator.plugins[1] },
        ],
    });

    // Override languages
    Object.defineProperty(navigator, 'languages', {
        get: () => ['en-US', 'en'],
    });

    // Override vendor
    Object.defineProperty(navigator, 'vendor', {
        get: () => 'Google Inc.',
    });

    // Hide chrome automation
    if (window.chrome) {
        delete window.chrome.csi;
        delete window.chrome.loadTimes;
    }
    // Ensure chrome.runtime exists (some detection checks for it)
    window.chrome = window.chrome || {};
    window.chrome.runtime = window.chrome.runtime || {};

    // Override notification API
    window.Notification = function(title, options) {
        return { permission: 'default' };
    };
    window.Notification.permission = 'default';
    window.Notification.requestPermission = () => Promise.resolve('default');

    // Mask iframe contentWindow for same-origin iframes (common detection vector)
    const oldCreateElement = document.createElement;
    document.createElement = function(tagName, options) {
        const el = oldCreateElement.call(this, tagName, options);
        if (tagName.toLowerCase() === 'iframe') {
            try {
                Object.defineProperty(el, 'contentWindow', {
                    get: function() {
                        const win = this.contentWindow;
                        if (win) delete win.navigator.webdriver;
                        return win;
                    }
                });
            } catch (e) {}
        }
        return el;
    };

    // Override navigator.connection (some bots don't have it)
    Object.defineProperty(navigator, 'connection', {
        get: () => ({ effectiveType: '4g', downlink: 10, rtt: 50 }),
    });
})();
"#;

/// Enable stealth mode by injecting script on every new document
pub async fn enable_stealth(cdp_client: &Arc<CdpClient>) -> Result<()> {
    let params = json!({
        "source": STEALTH_SCRIPT,
    });
    cdp_client.send_command("Page.addScriptToEvaluateOnNewDocument", params).await?;
    Ok(())
}

/// Chrome flags for stealth mode
pub fn stealth_chrome_flags() -> Vec<String> {
    vec![
        "--disable-blink-features=AutomationControlled".to_string(),
    ]
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_stealth_script_contains_key_patches() {
        assert!(STEALTH_SCRIPT.contains("webdriver"));
        assert!(STEALTH_SCRIPT.contains("plugins"));
        assert!(STEALTH_SCRIPT.contains("mimeTypes"));
        assert!(STEALTH_SCRIPT.contains("languages"));
        assert!(STEALTH_SCRIPT.contains("permissions.query"));
        assert!(STEALTH_SCRIPT.contains("chrome.runtime"));
        assert!(STEALTH_SCRIPT.contains("navigator.connection"));
    }

    #[test]
    fn test_stealth_flags() {
        let flags = stealth_chrome_flags();
        assert!(flags.contains(&"--disable-blink-features=AutomationControlled".to_string()));
    }
}