use crate::browser::cdp::CdpClient;
use crate::error::Result;
use serde_json::json;
use std::sync::Arc;
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 }),
});
})();
"#;
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(())
}
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()));
}
}