use drission::prelude::*;
use serde_json::Value;
#[tokio::main]
async fn main() -> drission::Result<()> {
tracing_subscriber::fmt().with_env_filter("warn").init();
let browser = Browser::launch(BrowserOptions::new().headless(true)).await?;
let tab = browser.latest_tab().await?;
let mut probe = tab.dump_env().start().await?;
tab.get("about:blank").await?;
tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
let dump = probe.collect().await?;
let mut chk = Checker::default();
let stealth = tab
.run_js(
"({fetch: ('' + window.fetch), open: ('' + XMLHttpRequest.prototype.open), ts: ('' + Function.prototype.toString)})",
)
.await?;
let is_native = |k: &str| {
stealth
.get(k)
.and_then(Value::as_str)
.is_some_and(|s| s.contains("[native code]"))
};
println!("==== 反 hook 检测(toString 自报 native) ====");
println!(
" ('' + fetch) = {}",
stealth["fetch"].as_str().unwrap_or("").replace('\n', " ")
);
chk.ok("fetch.toString 显示 [native code]", is_native("fetch"));
chk.ok("XHR.open.toString 显示 [native code]", is_native("open"));
chk.ok(
"Function.prototype.toString 自身也显示 native",
is_native("ts"),
);
let fp = dump.seed.get("fingerprint").cloned().unwrap_or(Value::Null);
let canvas_ok = fp
.pointer("/canvas/supported")
.and_then(Value::as_bool)
.unwrap_or(false);
let webgl_ok = fp
.pointer("/webgl/supported")
.and_then(Value::as_bool)
.unwrap_or(false);
let audio_ok = fp
.pointer("/audio/supported")
.and_then(Value::as_bool)
.unwrap_or(false);
let fonts_ok = fp
.pointer("/fonts/supported")
.and_then(Value::as_bool)
.unwrap_or(false);
let pixels_ok = fp
.pointer("/canvasPixels/supported")
.and_then(Value::as_bool)
.unwrap_or(false);
let rtc_ok = fp
.pointer("/rtc/supported")
.and_then(Value::as_bool)
.unwrap_or(false);
let plugins_n = dump
.seed
.pointer("/navigator/plugins")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0);
let mimes_n = dump
.seed
.pointer("/navigator/mimeTypes")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0);
println!("==== 采集到的指纹 ====");
println!(
" canvas : supported={canvas_ok} dataURL.len={}",
fp.pointer("/canvas/dataURL")
.and_then(Value::as_str)
.map(str::len)
.unwrap_or(0)
);
println!(
" webgl : supported={webgl_ok} vendor={:?} renderer={:?} ext={}",
fp.pointer("/webgl/unmaskedVendor")
.and_then(Value::as_str)
.unwrap_or("-"),
fp.pointer("/webgl/unmaskedRenderer")
.and_then(Value::as_str)
.unwrap_or("-"),
fp.pointer("/webgl/extensions")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0),
);
println!(
" audio : supported={audio_ok} sampleRate={:?} sum={:?}",
fp.pointer("/audio/sampleRate")
.and_then(Value::as_f64)
.unwrap_or(0.0),
fp.pointer("/audio/sum")
.and_then(Value::as_f64)
.unwrap_or(0.0),
);
println!(
" fonts : supported={fonts_ok} detected={}",
fp.pointer("/fonts/detected")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0),
);
println!(
" pixels : supported={pixels_ok} {}x{} hash={}",
fp.pointer("/canvasPixels/width")
.and_then(Value::as_u64)
.unwrap_or(0),
fp.pointer("/canvasPixels/height")
.and_then(Value::as_u64)
.unwrap_or(0),
fp.pointer("/canvasPixels/hash")
.and_then(Value::as_str)
.unwrap_or("-"),
);
println!(
" rtc : supported={rtc_ok} audioCodecs={} videoCodecs={}",
fp.pointer("/rtc/audioCodecs")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0),
fp.pointer("/rtc/videoCodecs")
.and_then(Value::as_array)
.map(Vec::len)
.unwrap_or(0),
);
println!(" plugins: {plugins_n} mimeTypes: {mimes_n}");
chk.ok("canvas 指纹已采集", canvas_ok);
chk.ok("fonts 枚举已采集", fonts_ok);
chk.ok("canvasPixels(getImageData)指纹已采集", pixels_ok);
let proj = std::env::current_dir()?.join("dump-env-fp");
let _ = std::fs::remove_dir_all(&proj);
dump.export_project(&proj, EnvScope::Full)?;
println!("\n==== 已导出补环境工程: {} ====", proj.display());
for f in [
"env.js",
"index.js",
"demo.js",
"verify.js",
"package.json",
"README.md",
"seed.json",
"signers.json",
] {
let exists = proj.join(f).exists();
chk.ok(&format!("工程含 {f}"), exists);
}
chk.ok("工程含 signer/ 目录", proj.join("signer").is_dir());
let report = dump.verify(&tab, &proj, EnvScope::Full).await?;
if let Some(err) = report.get("error").and_then(Value::as_str) {
println!("\n[库内 verify] 跳过(需要 node): {err}");
} else {
let pass = report["pass"].as_u64().unwrap_or(0);
let fail = report["fail"].as_u64().unwrap_or(0);
let total = report["total"].as_u64().unwrap_or(0);
println!("\n==== 验证一 · 库内同构双跑: {pass}/{total} 字段一致 ====");
if fail != 0
&& let Some(arr) = report["fields"].as_array()
{
for f in arr.iter().filter(|f| !f["ok"].as_bool().unwrap_or(true)) {
println!(
" ✗ {} : 浏览器={} | env.js={}",
f["field"], f["browser"], f["node"]
);
}
}
chk.ok("库内 verify 全字段一致", fail == 0 && total >= 5);
let has_field = |k: &str| {
report["fields"]
.as_array()
.is_some_and(|a| a.iter().any(|f| f["field"] == k))
};
if canvas_ok {
chk.ok("verify 覆盖 canvas.dataURL", has_field("canvas.dataURL"));
}
if webgl_ok {
chk.ok(
"verify 覆盖 webgl.unmaskedVendor",
has_field("webgl.unmaskedVendor"),
);
}
if audio_ok {
chk.ok("verify 覆盖 audio.sum", has_field("audio.sum"));
}
if fonts_ok {
chk.ok("verify 覆盖 fonts.hash", has_field("fonts.hash"));
}
if pixels_ok {
chk.ok(
"verify 覆盖 canvasPixels.hash",
has_field("canvasPixels.hash"),
);
}
chk.ok("verify 覆盖 rtc.supported", has_field("rtc.supported"));
chk.ok(
"verify 覆盖 navigator.pluginsCount",
has_field("navigator.pluginsCount"),
);
}
match std::process::Command::new("node")
.arg("verify.js")
.current_dir(&proj)
.output()
{
Ok(out) => {
let stdout = String::from_utf8_lossy(&out.stdout);
print!("\n==== 验证二 · 导出工程 node verify.js ====\n{stdout}");
if !out.status.success() {
eprintln!("{}", String::from_utf8_lossy(&out.stderr));
}
chk.ok("工程 verify.js 全部一致", out.status.success());
}
Err(e) => println!("\n[工程 verify.js] 跳过(需要 node): {e}"),
}
browser.quit().await?;
println!();
if chk.failed == 0 {
println!("ALL CHECKS PASSED ({} 项)", chk.passed);
Ok(())
} else {
eprintln!(
"FAILED: {} 项未通过 / 共 {}",
chk.failed,
chk.passed + chk.failed
);
std::process::exit(1);
}
}
#[derive(Default)]
struct Checker {
passed: usize,
failed: usize,
}
impl Checker {
fn ok(&mut self, name: &str, cond: bool) {
if cond {
self.passed += 1;
println!(" ✓ {name}");
} else {
self.failed += 1;
println!(" ✗ {name}");
}
}
}