pub(crate) fn stealth_args() -> Vec<String> {
let v = vec![
"--disable-blink-features=AutomationControlled".to_string(),
"--no-first-run".to_string(),
"--no-default-browser-check".to_string(),
"--no-service-autorun".to_string(),
"--password-store=basic".to_string(),
"--disable-popup-blocking".to_string(),
"--disable-background-networking".to_string(),
"--disable-features=Translate,OptimizationHints,MediaRouter,InterestFeedContentSuggestions"
.to_string(),
];
#[cfg(target_os = "macos")]
let v = {
let mut v = v;
v.push("--use-mock-keychain".to_string());
v
};
v
}
pub(crate) const STEALTH_JS: &str = r#"(function () {
try {
if (navigator.webdriver === true) {
Object.defineProperty(Object.getPrototypeOf(navigator), 'webdriver', {
get: function () { return false; },
configurable: true
});
}
} catch (e) {}
})();"#;
pub(crate) fn parse_chrome_major(s: &str) -> Option<u32> {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i].is_ascii_digit() {
let start = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if i < bytes.len() && bytes[i] == b'.' {
return s[start..i].parse::<u32>().ok();
}
} else {
i += 1;
}
}
None
}
pub(crate) fn reduced_ua(major: u32) -> String {
match std::env::consts::OS {
"macos" => format!(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{major}.0.0.0 Safari/537.36"
),
"windows" => format!(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{major}.0.0.0 Safari/537.36"
),
_ => format!(
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/{major}.0.0.0 Safari/537.36"
),
}
}
pub(crate) fn headless_screen_js() -> String {
let (w, h, avail_h) = (1920u32, 1080u32, 1040u32);
format!(
r#"(function(){{
try {{
var def = function(o,p,v){{ try {{ Object.defineProperty(o,p,{{get:function(){{return v;}},configurable:true}}); }} catch(e){{}} }};
def(screen,'width',{w}); def(screen,'height',{h});
def(screen,'availWidth',{w}); def(screen,'availHeight',{avail_h});
def(screen,'availLeft',0); def(screen,'availTop',0);
}} catch(e){{}}
}})();"#
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stealth_args_have_core_flag() {
let a = stealth_args();
assert!(
a.iter()
.any(|s| s == "--disable-blink-features=AutomationControlled"),
"必须含关掉 AutomationControlled 的核心反检测参数"
);
assert!(!a.iter().any(|s| s == "--test-type"));
assert!(
!a.iter().any(|s| s.contains("enable-automation")),
"不得传 --enable-automation"
);
}
#[test]
fn stealth_js_is_guarded_noop() {
assert!(STEALTH_JS.contains("navigator.webdriver === true"));
assert!(!STEALTH_JS.contains("addEventListener"));
}
#[test]
fn version_parsing() {
assert_eq!(
parse_chrome_major("Google Chrome 149.0.7827.115 "),
Some(149)
);
assert_eq!(
parse_chrome_major("X HeadlessChrome/137.0.0.0 Y"),
Some(137)
);
assert_eq!(parse_chrome_major("no version here"), None);
}
#[test]
fn reduced_ua_has_no_headless_and_major() {
let ua = reduced_ua(149);
assert!(!ua.contains("Headless"));
assert!(ua.contains("Chrome/149.0.0.0"));
assert!(ua.starts_with("Mozilla/5.0"));
}
#[test]
fn headless_screen_js_overrides_800x600() {
let js = headless_screen_js();
assert!(js.contains("1920") && js.contains("1080"));
assert!(js.contains("screen") && js.contains("availWidth"));
assert!(!js.contains("800") && !js.contains("600"));
}
}