reasonkit_web/browser/
stealth.rs1use crate::error::{Error, Result};
7use chromiumoxide::cdp::browser_protocol::page::AddScriptToEvaluateOnNewDocumentParams;
8use chromiumoxide::Page;
9use tracing::{debug, instrument};
10
11pub struct StealthMode;
13
14impl StealthMode {
15 #[instrument(skip(page))]
17 pub async fn apply(page: &Page) -> Result<()> {
18 debug!("Applying stealth mode");
19
20 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 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 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 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 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 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 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 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 }