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 parse_chrome_full(s: &str) -> Option<String> {
let bytes = s.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i].is_ascii_digit() {
let start = i;
let mut dots = 0;
while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') {
if bytes[i] == b'.' {
dots += 1;
}
i += 1;
}
let seg = &s[start..i];
if dots >= 3 && !seg.ends_with('.') {
return Some(seg.trim_end_matches('.').to_string());
}
} else {
i += 1;
}
}
None
}
pub(crate) fn ua_brand_list(major: u32, full_version: &str, full: bool) -> Vec<(String, String)> {
const GREASEY_CHARS: [&str; 11] = [
" ", "(", ":", "-", ".", "/", ")", ";", "=", "?", "_",
];
const GREASED_VERSIONS: [&str; 3] = ["8", "99", "24"];
const ORDERS: [[usize; 3]; 6] = [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
];
let seed = major as usize;
let greasey_brand = format!(
"Not{}A{}Brand",
GREASEY_CHARS[seed % GREASEY_CHARS.len()],
GREASEY_CHARS[(seed + 1) % GREASEY_CHARS.len()]
);
let greasey_major = GREASED_VERSIONS[seed % GREASED_VERSIONS.len()];
let chrome_ver = if full {
full_version.to_string()
} else {
major.to_string()
};
let greasey_ver = if full {
format!("{greasey_major}.0.0.0")
} else {
greasey_major.to_string()
};
let base = [
(greasey_brand, greasey_ver),
("Chromium".to_string(), chrome_ver.clone()),
("Google Chrome".to_string(), chrome_ver),
];
let order = ORDERS[seed % ORDERS.len()];
let mut out: Vec<(String, String)> = vec![(String::new(), String::new()); 3];
for (i, item) in base.into_iter().enumerate() {
out[order[i]] = item;
}
out
}
pub(crate) fn ch_platform() -> &'static str {
match std::env::consts::OS {
"macos" => "macOS",
"windows" => "Windows",
_ => "Linux",
}
}
pub(crate) fn ch_architecture() -> &'static str {
match std::env::consts::ARCH {
"aarch64" | "arm" => "arm",
_ => "x86",
}
}
pub(crate) fn ch_bitness() -> &'static str {
if cfg!(target_pointer_width = "32") {
"32"
} else {
"64"
}
}
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 full_version_parsing() {
assert_eq!(
parse_chrome_full("Google Chrome 149.0.7827.115 "),
Some("149.0.7827.115".to_string())
);
assert_eq!(
parse_chrome_full("X HeadlessChrome/137.0.0.0 Y"),
Some("137.0.0.0".to_string())
);
assert_eq!(parse_chrome_full("Chrome 149"), None);
assert_eq!(parse_chrome_full("no version"), None);
}
#[test]
fn ua_brand_list_matches_real_chrome_149() {
let brands = ua_brand_list(149, "149.0.7827.115", false);
assert_eq!(
brands,
vec![
("Google Chrome".to_string(), "149".to_string()),
("Chromium".to_string(), "149".to_string()),
("Not)A;Brand".to_string(), "24".to_string()),
]
);
let full = ua_brand_list(149, "149.0.7827.115", true);
assert_eq!(
full,
vec![
("Google Chrome".to_string(), "149.0.7827.115".to_string()),
("Chromium".to_string(), "149.0.7827.115".to_string()),
("Not)A;Brand".to_string(), "24.0.0.0".to_string()),
]
);
}
#[test]
fn ch_fields_sane() {
assert!(["macOS", "Windows", "Linux"].contains(&ch_platform()));
assert!(["arm", "x86"].contains(&ch_architecture()));
assert!(["64", "32"].contains(&ch_bitness()));
}
#[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"));
}
}