reasonkit_web/browser/
stealth.rs

1//! Stealth mode for anti-detection
2//!
3//! This module provides techniques to make the automated browser appear
4//! more like a regular user browser, bypassing common bot detection.
5
6use crate::error::{Error, Result};
7use chromiumoxide::cdp::browser_protocol::page::AddScriptToEvaluateOnNewDocumentParams;
8use chromiumoxide::Page;
9use tracing::{debug, instrument};
10
11/// Stealth mode configuration and application
12pub struct StealthMode;
13
14impl StealthMode {
15    /// Apply all stealth techniques to a page
16    #[instrument(skip(page))]
17    pub async fn apply(page: &Page) -> Result<()> {
18        debug!("Applying stealth mode");
19
20        // Apply all stealth scripts
21        Self::hide_webdriver(page).await?;
22        Self::mock_chrome_runtime(page).await?;
23        Self::override_webgl(page).await?;
24        Self::mock_plugins(page).await?;
25        Self::mock_languages(page).await?;
26        Self::hide_automation_indicators(page).await?;
27
28        debug!("Stealth mode applied successfully");
29        Ok(())
30    }
31
32    /// Hide navigator.webdriver property
33    async fn hide_webdriver(page: &Page) -> Result<()> {
34        let script = r#"
35            Object.defineProperty(navigator, 'webdriver', {
36                get: () => undefined,
37                configurable: true
38            });
39        "#;
40        Self::inject_script(page, script).await
41    }
42
43    /// Mock Chrome runtime object
44    async fn mock_chrome_runtime(page: &Page) -> Result<()> {
45        let script = r#"
46            if (!window.chrome) {
47                window.chrome = {};
48            }
49            if (!window.chrome.runtime) {
50                window.chrome.runtime = {
51                    connect: function() {},
52                    sendMessage: function() {},
53                    onMessage: {
54                        addListener: function() {},
55                        removeListener: function() {}
56                    }
57                };
58            }
59        "#;
60        Self::inject_script(page, script).await
61    }
62
63    /// Override WebGL fingerprinting
64    async fn override_webgl(page: &Page) -> Result<()> {
65        let script = r"
66            const getParameterOriginal = WebGLRenderingContext.prototype.getParameter;
67            WebGLRenderingContext.prototype.getParameter = function(parameter) {
68                // UNMASKED_VENDOR_WEBGL
69                if (parameter === 37445) {
70                    return 'Intel Inc.';
71                }
72                // UNMASKED_RENDERER_WEBGL
73                if (parameter === 37446) {
74                    return 'Intel Iris OpenGL Engine';
75                }
76                return getParameterOriginal.call(this, parameter);
77            };
78
79            // WebGL2
80            if (typeof WebGL2RenderingContext !== 'undefined') {
81                const getParameter2Original = WebGL2RenderingContext.prototype.getParameter;
82                WebGL2RenderingContext.prototype.getParameter = function(parameter) {
83                    if (parameter === 37445) {
84                        return 'Intel Inc.';
85                    }
86                    if (parameter === 37446) {
87                        return 'Intel Iris OpenGL Engine';
88                    }
89                    return getParameter2Original.call(this, parameter);
90                };
91            }
92        ";
93        Self::inject_script(page, script).await
94    }
95
96    /// Mock navigator.plugins
97    async fn mock_plugins(page: &Page) -> Result<()> {
98        let script = r#"
99            Object.defineProperty(navigator, 'plugins', {
100                get: () => {
101                    const plugins = [
102                        { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer' },
103                        { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai' },
104                        { name: 'Native Client', filename: 'internal-nacl-plugin' },
105                        { name: 'Chromium PDF Plugin', filename: 'internal-pdf-viewer' },
106                        { name: 'Chromium PDF Viewer', filename: 'internal-pdf-viewer' }
107                    ];
108                    plugins.length = 5;
109                    plugins.item = (i) => plugins[i];
110                    plugins.namedItem = (name) => plugins.find(p => p.name === name);
111                    plugins.refresh = () => {};
112                    return plugins;
113                },
114                configurable: true
115            });
116        "#;
117        Self::inject_script(page, script).await
118    }
119
120    /// Mock navigator.languages
121    async fn mock_languages(page: &Page) -> Result<()> {
122        let script = r#"
123            Object.defineProperty(navigator, 'languages', {
124                get: () => ['en-US', 'en', 'es'],
125                configurable: true
126            });
127
128            Object.defineProperty(navigator, 'language', {
129                get: () => 'en-US',
130                configurable: true
131            });
132        "#;
133        Self::inject_script(page, script).await
134    }
135
136    /// Hide other automation indicators
137    async fn hide_automation_indicators(page: &Page) -> Result<()> {
138        let script = r#"
139            // Hide automation flags
140            Object.defineProperty(navigator, 'maxTouchPoints', {
141                get: () => 0,
142                configurable: true
143            });
144
145            // Override permissions API
146            if (navigator.permissions) {
147                const originalQuery = navigator.permissions.query;
148                navigator.permissions.query = (parameters) => (
149                    parameters.name === 'notifications' ?
150                        Promise.resolve({ state: Notification.permission }) :
151                        originalQuery(parameters)
152                );
153            }
154
155            // Mock connection type
156            if (!navigator.connection) {
157                Object.defineProperty(navigator, 'connection', {
158                    get: () => ({
159                        effectiveType: '4g',
160                        rtt: 50,
161                        downlink: 10,
162                        saveData: false
163                    }),
164                    configurable: true
165                });
166            }
167
168            // Hide headless indicators in User-Agent Client Hints
169            if (navigator.userAgentData) {
170                Object.defineProperty(navigator.userAgentData, 'brands', {
171                    get: () => [
172                        { brand: 'Google Chrome', version: '120' },
173                        { brand: 'Chromium', version: '120' },
174                        { brand: 'Not_A Brand', version: '24' }
175                    ],
176                    configurable: true
177                });
178            }
179        "#;
180        Self::inject_script(page, script).await
181    }
182
183    /// Inject a script to run on new document
184    async fn inject_script(page: &Page, script: &str) -> Result<()> {
185        let params = AddScriptToEvaluateOnNewDocumentParams::builder()
186            .source(script)
187            .build()
188            .map_err(|e| Error::cdp(format!("Failed to build script params: {}", e)))?;
189
190        page.execute(params)
191            .await
192            .map_err(|e| Error::cdp(format!("Failed to inject script: {}", e)))?;
193
194        Ok(())
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    // Stealth mode tests require a running browser
201    // These are integration tests that will be in tests/browser_tests.rs
202}