drission 0.3.0

Rust 反检测浏览器自动化 + 内置验证码识别:ddddocr 离线 OCR 与图片滑块缺口距离(极验/顶象),默认 Camoufox/Firefox、自动过 Cloudflare 盾、高并发爬虫与 XHR 监听拦截,DrissionPage 风格 API。Anti-detect browser automation in Rust with built-in ddddocr captcha OCR + slider-gap (GeeTest) solving, Camoufox, Cloudflare bypass — a Rust DrissionPage.
//! 吐环境深化 · canvas/webgl/audio + 字体/像素 canvas/WebRTC/plugins 指纹补环境 + 一键导出工程(离线自验证)。
//!
//! 完全离线(`data:` 页,不出网):
//!   1) `tab.dump_env()` 导航前注入探针(已含 Function.prototype.toString 防 hook 检测);
//!   2) 采集**全量种子**——canvas / webgl / audioContext / **字体枚举 / 像素级 getImageData / WebRTC / plugins+mimeTypes** 指纹;
//!   3) `export_project()` 一键导出**可直接 `node` 运行的补环境工程**(npm 包 + 纯算签名 demo);
//!   4) 双重验证补环境忠实回放浏览器指纹:
//!      - 库内 `verify`:浏览器真实环境 vs Node 补环境沙箱,逐字段(含 canvas/webgl/audio);
//!      - 导出工程自带 `node verify.js`:env.js 回放 vs seed.json 录制值。
//!
//! 末行打印 ALL CHECKS PASSED(任一关键校验失败则进程非 0 退出)。
//!
//! 运行:`cargo run --example dump_env_fingerprint --no-default-features --features camoufox`

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?;
    // about:blank 是离线下最可靠、且支持 canvas/webgl/audio 与 init 脚本的文档。
    tab.get("about:blank").await?;
    // 等 OfflineAudioContext 渲染 + 指纹采集就绪。
    tokio::time::sleep(std::time::Duration::from_millis(1500)).await;
    let dump = probe.collect().await?;

    let mut chk = Checker::default();

    // —— 反 hook 检测:探针 hook 了 fetch/XHR,但 Function.prototype.toString 让它们自报 native ——
    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);
    // 里程碑 47 新增指纹:字体枚举 / 像素 canvas / WebRTC / plugins+mimeTypes。
    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}");

    // 至少要采到 canvas(headless 下 webgl/audio 视构建可能缺失,缺则跳过其校验但不算失败)。
    chk.ok("canvas 指纹已采集", canvas_ok);
    // 字体枚举与像素 canvas 在 about:blank 同源下应可用(getImageData 不 taint)。
    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());

    // —— 验证一:库内同构双跑(浏览器真实环境 vs Node 补环境,含指纹) ——
    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"),
            );
        }
        // rtc.supported / navigator.plugins* 始终参与比对(里程碑 47)。
        chk.ok("verify 覆盖 rtc.supported", has_field("rtc.supported"));
        chk.ok(
            "verify 覆盖 navigator.pluginsCount",
            has_field("navigator.pluginsCount"),
        );
    }

    // —— 验证二:导出工程自带 node verify.js(env.js 回放 vs seed.json) ——
    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}");
        }
    }
}