#![allow(dead_code)]
use rustenium::browsers::cdp_browser::CdpBrowser;
use rustenium::browsers::chrome::browser::ChromeConfig;
use rustenium::cdp::target_manager::InitScript;
use rustenium_identity::{IdentityConfig, IdentitySession, preset};
use std::time::Duration;
fn chrome_config() -> ChromeConfig {
ChromeConfig {
enable_bidi: false,
enable_cdp: true,
chrome_executable_path: std::env::var("CHROME_PATH").ok(),
..Default::default()
}
}
pub const NETWORK_TAP: &str = r#"
window.__net = [];
(function () {
var origFetch = window.fetch;
window.fetch = function (input, init) {
var url = (typeof input === 'string') ? input : (input && input.url) || String(input);
var body = (init && init.body) || null;
return origFetch.apply(this, arguments).then(function (res) {
try {
res.clone().text().then(function (txt) {
window.__net.push({ url: url, req: body ? String(body) : null, status: res.status, res: txt });
});
} catch (e) {}
return res;
});
};
var origOpen = XMLHttpRequest.prototype.open;
var origSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function (m, u) { this.__u = u; return origOpen.apply(this, arguments); };
XMLHttpRequest.prototype.send = function (b) {
var self = this;
this.addEventListener('load', function () {
window.__net.push({ url: self.__u, req: b ? String(b) : null, status: self.status, res: self.responseText });
});
return origSend.apply(this, arguments);
};
})();
"#;
pub enum Detector {
Identity(IdentitySession),
Baseline(rustenium::browsers::chrome::browser::ChromeBrowser),
}
impl Detector {
pub async fn open(preset_id: u64, url: &str) -> Detector {
Detector::open_inner(preset_id, url, false).await
}
pub async fn open_direct(preset_id: u64, url: &str) -> Detector {
Detector::open_inner(preset_id, url, true).await
}
async fn open_inner(preset_id: u64, url: &str, force_direct: bool) -> Detector {
let mut identity = match std::env::var("IDENTITY_JSON") {
Ok(path) => {
let raw = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("cannot read {path}: {e}"));
println!("identity from {path}");
rustenium_identity::Identity::from_json(&raw)
.unwrap_or_else(|e| panic!("cannot parse {path}: {e}"))
}
Err(_) => preset::get_by_id(preset_id).expect("preset id out of range"),
};
if let Ok(v) = std::env::var("PERSONA_VERSION") {
if let Some(parts) = rustenium_identity::ua::parse_version_parts(&v) {
identity.browser_version = parts;
}
}
if force_direct {
identity.proxy = None;
} else if let Ok(p) = std::env::var("PROXY_URL") {
identity.proxy = Some(p);
}
println!(
"launching preset #{preset_id}: {:?} {} / {:?} {:?} / {}",
identity.os,
identity.os_version,
identity.browser,
identity.browser_version,
identity.gpu.webgl_renderer,
);
let mut d = if std::env::var("NO_IDENTITY").is_ok() {
println!(" NO_IDENTITY set — launching a plain browser, nothing applied");
let mut cfg = chrome_config();
cfg.browser_flags = Some(vec![
"--disable-blink-features=AutomationControlled".to_string(),
]);
Detector::Baseline(rustenium::browsers::chrome::browser::ChromeBrowser::new(cfg).await)
} else {
Detector::Identity(
IdentitySession::launch(IdentityConfig::new(identity, chrome_config()))
.await
.expect("failed to launch identity session"),
)
};
d.browser_mut()
.add_init_script(InitScript { page: Some(NETWORK_TAP.into()), worker: None })
.await;
d.browser_mut()
.navigate(url)
.await
.unwrap_or_else(|e| panic!("navigate to {url} failed: {e:?}"));
d
}
fn browser_mut(&mut self) -> &mut rustenium::browsers::chrome::browser::ChromeBrowser {
match self {
Detector::Identity(s) => s.browser_mut(),
Detector::Baseline(b) => b,
}
}
pub async fn eval(&mut self, expr: &str) -> serde_json::Value {
self.evaluate(expr, false).await
}
pub async fn eval_async(&mut self, expr: &str) -> serde_json::Value {
self.evaluate(expr, true).await
}
async fn evaluate(&mut self, expr: &str, await_promise: bool) -> serde_json::Value {
let raw = self
.browser_mut()
.evaluate_script(expr, await_promise)
.await
.map(|v| v.result.value.and_then(|x| x.as_str().map(str::to_string)))
.unwrap_or(None)
.unwrap_or_default();
serde_json::from_str(&raw).unwrap_or(serde_json::Value::Null)
}
pub async fn wait_until(&mut self, expr: &str, timeout: Duration) -> bool {
let deadline = std::time::Instant::now() + timeout;
while std::time::Instant::now() < deadline {
tokio::time::sleep(Duration::from_secs(2)).await;
if let Ok(v) = self.browser_mut().evaluate_script(expr, false).await
&& v.result.value.as_ref().and_then(|x| x.as_str()) == Some("yes")
{
return true;
}
print!(".");
}
println!();
false
}
pub async fn traffic(&mut self) -> serde_json::Value {
self.eval(
r#"JSON.stringify((window.__net || []).map(n => ({
url: n.url, status: n.status,
req: n.req ? n.req.slice(0, 4000) : null,
res: n.res ? n.res.slice(0, 4000) : null,
})))"#,
)
.await
}
pub fn identity(&self) -> &rustenium_identity::Identity {
match self {
Detector::Identity(s) => s.identity(),
Detector::Baseline(_) => panic!("no identity was applied in this run"),
}
}
pub async fn close(self) {
match self {
Detector::Identity(s) => {
s.close().await;
}
Detector::Baseline(b) => {
use rustenium::browsers::BidiBrowser;
let _ = b.close().await;
}
}
}
}
pub fn dump(heading: &str, value: &serde_json::Value) {
println!("\n--- {heading} ---");
println!("{}", serde_json::to_string_pretty(value).unwrap_or_default());
}