#![cfg(feature = "cdp-backend")]
use std::time::Duration;
use crawlex::render::chrome::browser::{Browser, BrowserConfig, HeadlessMode};
use crawlex::render::chrome_protocol::cdp::browser_protocol::page::AddScriptToEvaluateOnNewDocumentParams;
use futures::StreamExt;
fn system_chrome() -> Option<String> {
for path in [
"/usr/bin/google-chrome",
"/usr/bin/google-chrome-stable",
"/usr/bin/chromium",
"/usr/bin/chromium-browser",
"/snap/bin/chromium",
] {
if std::path::Path::new(path).exists() {
return Some(path.into());
}
}
None
}
async fn launch(stealth: bool) -> (Browser, tempfile::TempDir) {
let exec = system_chrome().expect("requires system Chrome installed");
let tmp = tempfile::tempdir().expect("tmp user_data_dir");
let cfg = BrowserConfig::builder()
.chrome_executable(exec)
.headless_mode(HeadlessMode::New)
.no_sandbox()
.user_data_dir(tmp.path())
.stealth_runtime_enable_skip(stealth)
.build()
.expect("build browser config");
let (browser, mut handler) = Browser::launch(cfg).await.expect("launch browser");
tokio::spawn(async move { while let Some(_ev) = handler.next().await {} });
(browser, tmp)
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires Chromium; run with --ignored"]
async fn stealth_mode_evaluate_does_not_see_main_world_globals() {
let (browser, _tmp) = launch(true).await;
let page = browser.new_page("about:blank").await.expect("new_page");
let add_script = AddScriptToEvaluateOnNewDocumentParams::builder()
.source("window.__main_written = true")
.build()
.expect("build addScript");
page.execute(add_script)
.await
.expect("addScriptToEvaluateOnNewDocument");
page.reload().await.expect("reload");
tokio::time::sleep(Duration::from_millis(500)).await;
let sum: i64 = page
.evaluate("1 + 1")
.await
.expect("basic evaluate must succeed under stealth")
.into_value()
.expect("into_value i64");
assert_eq!(sum, 2);
let flag: String = page
.evaluate("typeof window.__main_written")
.await
.expect("probe evaluate")
.into_value()
.expect("into_value");
assert_eq!(
flag, "undefined",
"evaluate in stealth mode must run in the isolated world, which \
does not share `window` with the main world. Got {flag:?}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires Chromium; run with --ignored"]
async fn default_mode_evaluate_sees_main_world_globals() {
let (browser, _tmp) = launch(false).await;
let page = browser.new_page("about:blank").await.expect("new_page");
let add_script = AddScriptToEvaluateOnNewDocumentParams::builder()
.source("window.__main_written = true")
.build()
.expect("build addScript");
page.execute(add_script)
.await
.expect("addScriptToEvaluateOnNewDocument");
page.reload().await.expect("reload");
tokio::time::sleep(Duration::from_millis(500)).await;
let flag: bool = page
.evaluate("window.__main_written === true")
.await
.expect("probe evaluate")
.into_value()
.expect("into_value");
assert!(
flag,
"default mode must run evaluate in the main world and see its globals"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires Chromium; run with --ignored"]
async fn stealth_mode_does_not_trigger_prepare_stack_trace() {
let (browser, _tmp) = launch(true).await;
let page = browser.new_page("about:blank").await.expect("new_page");
let probe = r#"
window.__err_hit = 0;
Error.prepareStackTrace = function(err, stack) {
window.__err_hit = (window.__err_hit || 0) + 1;
return stack;
};
// Force one throw so we know the hook itself wires up correctly
// in the main world, but DON'T count that one — we clear after.
try { null.x } catch (_) {}
window.__err_hit = 0;
"#;
let add_script = AddScriptToEvaluateOnNewDocumentParams::builder()
.source(probe)
.build()
.expect("build addScript");
page.execute(add_script)
.await
.expect("addScriptToEvaluateOnNewDocument");
page.reload().await.expect("reload");
tokio::time::sleep(Duration::from_millis(1000)).await;
let capture = AddScriptToEvaluateOnNewDocumentParams::builder()
.source(
"document.documentElement.setAttribute('data-err-hit', \
String(window.__err_hit|0))",
)
.build()
.expect("build capture script");
page.execute(capture).await.expect("addScript capture");
page.reload().await.expect("reload2");
tokio::time::sleep(Duration::from_millis(1000)).await;
let hits: i64 = page
.evaluate(
"parseInt(document.documentElement.getAttribute('data-err-hit') \
|| '-1', 10)",
)
.await
.expect("read data-err-hit")
.into_value()
.expect("into_value i64");
eprintln!("brotector probe: Error.prepareStackTrace fired {hits} times under stealth");
assert!(
hits < 100,
"unreasonably high prepareStackTrace count under stealth ({hits}) \
— this likely means Runtime.enable is attached or the probe is \
in an infinite-recursion. Investigate."
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires Chromium; run with --ignored"]
async fn default_mode_prepare_stack_trace_control() {
let (browser, _tmp) = launch(false).await;
let page = browser.new_page("about:blank").await.expect("new_page");
let probe = r#"
window.__err_hit = 0;
Error.prepareStackTrace = function(err, stack) {
window.__err_hit = (window.__err_hit || 0) + 1;
return stack;
};
"#;
let add_script = AddScriptToEvaluateOnNewDocumentParams::builder()
.source(probe)
.build()
.expect("build addScript");
page.execute(add_script)
.await
.expect("addScriptToEvaluateOnNewDocument");
page.reload().await.expect("reload");
tokio::time::sleep(Duration::from_millis(1000)).await;
let hits: i64 = page
.evaluate("window.__err_hit | 0")
.await
.expect("probe read")
.into_value()
.expect("into_value");
eprintln!(
"[default-mode control] Error.prepareStackTrace hits = {hits} \
(expect >=0; >0 would prove the probe discriminates stealth vs \
default on this Chrome build)"
);
assert!(
hits >= 0,
"counter must be a non-negative integer, got {hits}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires Chromium; run with --ignored"]
async fn stealth_mode_isolated_world_rebinds_after_navigation() {
let (browser, _tmp) = launch(true).await;
let page = browser.new_page("about:blank").await.expect("new_page");
let sum: i64 = page
.evaluate("1 + 1")
.await
.expect("first-bind evaluate must succeed under stealth")
.into_value()
.expect("into_value i64");
assert_eq!(sum, 2, "initial isolated-world bind is broken");
page.goto("data:text/html,<html><body>X</body></html>")
.await
.expect("goto data url");
tokio::time::sleep(Duration::from_millis(500)).await;
let body: String = page
.evaluate("document.body.innerText")
.await
.expect(
"post-navigation evaluate must succeed — if this is \"Cannot \
find context with specified id\", P0.4 fase 5 (isolated-world \
re-bind on frame swap) is regressed",
)
.into_value()
.expect("into_value String");
assert_eq!(
body.trim(),
"X",
"post-navigation evaluate returned {body:?}, expected \"X\". The \
isolated world re-bound but is pointing at the wrong frame/document."
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[ignore = "requires Chromium; run with --ignored"]
async fn stealth_mode_navigator_webdriver_absent_enabled() {
let (browser, _tmp) = launch(true).await;
let page = browser.new_page("about:blank").await.expect("new_page");
tokio::time::sleep(Duration::from_millis(200)).await;
let has_webdriver: bool = page
.evaluate("'webdriver' in navigator && navigator.webdriver === true")
.await
.expect("probe navigator.webdriver")
.into_value()
.expect("into_value bool");
assert!(
!has_webdriver,
"navigator.webdriver is TRUE under stealth. \
--disable-blink-features=AutomationControlled is not taking effect. \
Every automated tooling fingerprint will flag this browser."
);
}