rustenium-identity 0.1.14

A versatile stealth overlay for rustenium
Documentation
//! BrowserScan — robot detection and the private-mode verdict.
//!
//! Its incognito row is `detectIncognito` shipped verbatim, and the whole Chrome
//! branch is one comparison:
//!
//! ```text
//! isPrivate = round(quota / MiB) < 2 * round(performance.memory.jsHeapSizeLimit / MiB)
//! ```
//!
//! Both sides come from the machine rather than the identity: `quota` is derived
//! from the volume holding Chrome's profile directory, and `jsHeapSizeLimit` from
//! physical RAM. A profile on a small filesystem — `/tmp` is a tmpfs at half of
//! RAM on Debian 13 — reports private on every otherwise-perfect session, so
//! `incognito_is_not_reported` is really a check on where the profile lives.
//!
//! `cargo test --test browserscan -- --ignored --nocapture`

mod common;

use common::{Detector, dump};
use std::time::Duration;

const URL: &str = "https://www.browserscan.net/";

const READY: &str = r#"(() => {
    const t = document.body ? document.body.innerText : '';
    return /Robot|Incognito|IP address/i.test(t) ? 'yes' : 'no';
})()"#;

/// Reproduces detectIncognito's own arithmetic so a failure names the numbers,
/// not just the verdict. BrowserScan renders the conclusion; this renders why.
const QUOTA: &str = r#"(async () => {
  const out = {};
  try { out.jsHeapSizeLimit = performance.memory.jsHeapSizeLimit; } catch (e) {}
  const quota = await new Promise((r) => {
    try {
      if (navigator.webkitTemporaryStorage && navigator.webkitTemporaryStorage.queryUsageAndQuota) {
        navigator.webkitTemporaryStorage.queryUsageAndQuota((u, q) => r(q), () => r(null));
      } else if (navigator.storage && navigator.storage.estimate) {
        navigator.storage.estimate().then((e) => r(e.quota)).catch(() => r(null));
      } else r(null);
    } catch (e) { r(null); }
  });
  out.quota = quota;
  if (quota && out.jsHeapSizeLimit) {
    out.quotaMiB = Math.round(quota / 1048576);
    out.barMiB = Math.round(out.jsHeapSizeLimit / 1048576) * 2;
    out.isPrivate = out.quotaMiB < out.barMiB;
  }
  return JSON.stringify(out);
})()"#;

/// The summary rows, matched by their own text — the CSS module class names are
/// fully hashed, so nothing else about the markup is stable.
const SCRAPE: &str = r#"(() => {
  const text = document.body ? document.body.innerText : '';
  const after = (label) => {
    const re = new RegExp(label + '\\s*\\n\\s*([^\\n]{1,60})', 'i');
    const m = text.match(re);
    return m ? m[1].trim() : null;
  };
  return JSON.stringify({
    incognito: after('Incognito'),
    robot: after('Robot'),
    webdriver: after('WebDriver'),
    userAgent: after('User Agent'),
    platform: after('Platform'),
    net: (window.__net || []).map((n) => ({ url: n.url, status: n.status })),
  });
})()"#;

#[tokio::test]
#[ignore = "launches a browser and hits the network"]
async fn incognito_is_not_reported() {
    let mut d = Detector::open(1, URL).await;
    d.wait_until(READY, Duration::from_secs(90)).await;

    let quota = d.eval_async(QUOTA).await;
    dump("detectIncognito arithmetic", &quota);
    assert!(
        quota["quota"].is_number(),
        "could not read the storage quota — the probe returned {quota}"
    );

    let report = d.eval(SCRAPE).await;
    dump("browserscan", &report);
    d.close().await;

    let is_private = quota["isPrivate"].as_bool();
    assert_ne!(
        is_private,
        Some(true),
        "browserscan reports incognito: quota {} MiB is under the {} MiB bar. \
         Chrome's profile is on a filesystem too small — check where user_data_dir \
         points (std::env::temp_dir() is /tmp, a tmpfs on Debian 13).",
        quota["quotaMiB"],
        quota["barMiB"],
    );

    if let Some(row) = report["incognito"].as_str() {
        assert!(
            !row.eq_ignore_ascii_case("yes"),
            "browserscan's incognito row says {row:?}"
        );
    }
}

#[tokio::test]
#[ignore = "launches a browser and hits the network"]
async fn robot_detection_passes() {
    let mut d = Detector::open(1, URL).await;
    d.wait_until(READY, Duration::from_secs(90)).await;

    let report = d.eval(SCRAPE).await;
    dump("browserscan", &report);
    let traffic = d.traffic().await;
    dump("traffic", &traffic);
    d.close().await;

    if let Some(robot) = report["robot"].as_str() {
        assert!(
            !robot.to_lowercase().contains("yes") && !robot.to_lowercase().contains("detected"),
            "browserscan's robot row says {robot:?}"
        );
    }
}