diff --git a/.github/ISSUE_TEMPLATE/01-launch-failure.yml b/.github/ISSUE_TEMPLATE/01-launch-failure.yml deleted file mode 100644 index 2c5451f..0000000 --- a/.github/ISSUE_TEMPLATE/01-launch-failure.yml +++ /dev/null @@ -1,98 +0,0 @@ -name: Launch failure -description: Browser or wrapper fails to start (install errors, missing deps, profile load fails, never reaches new_page) -title: "[launch] " -labels: ["bug", "launch-failure"] -body: - - type: markdown - attributes: - value: | - Use this when the browser never reaches a usable state. - If it starts and the bug appears on a site or clicking something, use the site/action template instead. - - - type: input - id: version - attributes: - label: Version - description: Output of `python -m invisible_playwright version`. - placeholder: 0.1.7 (binary firefox-7) - validations: - required: true - - - type: dropdown - id: os - attributes: - label: OS - options: - - Windows 10/11 x86_64 - - Linux x86_64 - - macOS (unsupported) - - Other - validations: - required: true - - - type: input - id: python - attributes: - label: Python - placeholder: 3.11.7 - validations: - required: true - - - type: input - id: install_cmd - attributes: - label: How you installed - placeholder: pip install invisible_playwright - validations: - required: true - - - type: textarea - id: snippet - attributes: - label: What you ran - description: Stop at the line that errors out. Redact creds. - render: python - value: | - from invisible_playwright import InvisiblePlaywright - with InvisiblePlaywright(seed=42) as browser: - ctx = browser.new_context() - validations: - required: true - - - type: textarea - id: traceback - attributes: - label: Full traceback - description: The whole stack trace verbatim. Don't summarize. - render: text - validations: - required: true - - - type: textarea - id: logs - attributes: - label: Extra logs - description: Output of `DEBUG=pw:browser* python yourscript.py 2>&1`. Optional but speeds things up. - render: text - validations: - required: false - - - type: textarea - id: tried - attributes: - label: What you already tried - description: Reinstall, clear cache, different Python version, different proxy, etc. - validations: - required: false - - - type: checkboxes - id: confirm - attributes: - label: Before submitting - options: - - label: Searched existing issues. - required: true - - label: On the latest released version. - required: true - - label: Removed credentials and personal paths from the snippet and logs. - required: true diff --git a/.github/ISSUE_TEMPLATE/02-site-or-action-bug.yml b/.github/ISSUE_TEMPLATE/02-site-or-action-bug.yml deleted file mode 100644 index 6c38de6..0000000 --- a/.github/ISSUE_TEMPLATE/02-site-or-action-bug.yml +++ /dev/null @@ -1,167 +0,0 @@ -name: Site or action bug -description: Browser starts fine but a navigation, click, evaluate, or other operation fails or behaves wrong -title: "[bug] " -labels: ["bug"] -body: - - type: markdown - attributes: - value: | - For bugs that happen after the browser is up. - If the browser never launches, use the launch failure template. - If a fingerprint detector flags the browser, use the stealth detection template. - - - type: input - id: version - attributes: - label: Version - description: Output of `python -m invisible_playwright version`. - placeholder: 0.1.7 (binary firefox-7) - validations: - required: true - - - type: dropdown - id: os - attributes: - label: OS - options: - - Windows 10/11 x86_64 - - Linux x86_64 - - macOS (unsupported) - - Other - validations: - required: true - - - type: input - id: python - attributes: - label: Python - placeholder: 3.11.7 - validations: - required: true - - - type: dropdown - id: headless - attributes: - label: headless= - description: Some bugs only repro on Windows headless=True (hidden alt-desktop path). - options: - - "True" - - "False" - validations: - required: true - - - type: dropdown - id: proxy - attributes: - label: Proxy - description: Sites often vary by IP geo (e.g. GDPR consent shows only on UK/EU). - options: - - No proxy (host network) - - Residential, UK/GB - - Residential, US - - Residential, other country (specify in notes) - - Datacenter (specify provider in notes) - validations: - required: true - - - type: dropdown - id: profile - attributes: - label: Profile dir - options: - - Fresh each run (no profile_dir) - - Persistent profile_dir, reusing across runs - - Persistent profile_dir, first run creating it - validations: - required: true - - - type: input - id: url - attributes: - label: URL - description: The exact URL passed to `page.goto`. Not "the homepage" — the literal string. - placeholder: https://id.sky.com/ - validations: - required: true - - - type: textarea - id: snippet - attributes: - label: Runnable reproduction - description: A complete snippet we can copy, paste, run. Stub creds with placeholders, keep everything else literal. - render: python - value: | - from invisible_playwright import InvisiblePlaywright - - with InvisiblePlaywright(seed=42, headless=True) as browser: - ctx = browser.new_context() - page = ctx.new_page() - page.goto("https://example.com/") - # the exact operation that fails: - page.click("button:has-text('Accept all')") - validations: - required: true - - - type: input - id: selector - attributes: - label: Selector or locator - description: The exact string passed to locator/click/frame_locator. Write N/A if not a selector bug. - placeholder: page.frame_locator("iframe[id^='sp_message_iframe_']").get_by_text("Accept all") - validations: - required: true - - - type: textarea - id: expected - attributes: - label: Expected - description: What should happen when the snippet runs? - validations: - required: true - - - type: textarea - id: actual - attributes: - label: Actual - description: What happens instead? Full traceback, error string verbatim, any page.on('crash') firing. - validations: - required: true - - - type: textarea - id: screenshot - attributes: - label: Screenshot - description: Drag-drop a screenshot if the bug is visual. Optional but useful. - validations: - required: false - - - type: textarea - id: logs - attributes: - label: Browser logs - description: Output of `DEBUG=pw:browser* python yourscript.py 2>&1 | tail -200`. Redact creds and real IPs. - render: text - validations: - required: false - - - type: textarea - id: notes - attributes: - label: Notes - description: Anything else, hypotheses, related issues, things you've already tried. - validations: - required: false - - - type: checkboxes - id: confirm - attributes: - label: Before submitting - options: - - label: Searched existing issues. - required: true - - label: On the latest released version. - required: true - - label: The snippet above runs end-to-end on a clean Python install. - required: true - - label: Removed credentials, proxy passwords, real IPs, personal file paths. - required: true diff --git a/.github/ISSUE_TEMPLATE/03-stealth-detection.yml b/.github/ISSUE_TEMPLATE/03-stealth-detection.yml deleted file mode 100644 index b2c5e1d..0000000 --- a/.github/ISSUE_TEMPLATE/03-stealth-detection.yml +++ /dev/null @@ -1,141 +0,0 @@ -name: Stealth detection -description: A fingerprint detector flagged the browser as a bot, VM, VPN, anti-detect, tampered, or otherwise non-human -title: "[detect] " -labels: ["bug", "stealth"] -body: - - type: markdown - attributes: - value: | - Use this when something detects the browser (Fingerprint Pro, CreepJS, BotD, reCAPTCHA, Cloudflare, sannysoft, etc). - Bugs in operations (clicks, navigation) go to the site/action template. - Browser failing to start goes to the launch failure template. - - - type: input - id: version - attributes: - label: Version - placeholder: 0.1.7 (binary firefox-7) - validations: - required: true - - - type: dropdown - id: os - attributes: - label: OS - options: - - Windows 10/11 x86_64 - - Linux x86_64 - - macOS (unsupported) - - Other - validations: - required: true - - - type: dropdown - id: headless - attributes: - label: headless= - options: - - "True" - - "False" - validations: - required: true - - - type: dropdown - id: proxy - attributes: - label: Proxy - description: Datacenter or wrong-country proxies trip most detectors regardless of the browser. Be honest about what you used. - options: - - No proxy (host network) - - Residential, matching target geo - - Residential, different geo than target - - Datacenter (specify provider in notes) - - Mobile / 4G - validations: - required: true - - - type: input - id: detector - attributes: - label: Detector name and URL - description: Exact site / service / product that flagged us. - placeholder: Fingerprint Pro — https://demo.fingerprint.com/playground - validations: - required: true - - - type: textarea - id: scores - attributes: - label: Detector verdict - description: Paste the relevant flags / scores verbatim. For Fingerprint Pro paste `bot`, `vpn`, `virtual_machine`, `tampering*`, `vm_ml_score`, `suspect_score`. For CreepJS the headless / lies / trust scores. For reCAPTCHA v3 the score number. - render: text - placeholder: | - bot: bad - vpn: true - virtual_machine: true - vm_ml_score: 0.74 - suspect_score: 22 - validations: - required: true - - - type: textarea - id: screenshot - attributes: - label: Screenshot of the detector result - description: Drag-drop a screenshot of the detector page so we see what you see. - validations: - required: true - - - type: textarea - id: snippet - attributes: - label: How you launched - description: The InvisiblePlaywright launch + navigation that produced the result above. Redact creds. - render: python - value: | - from invisible_playwright import InvisiblePlaywright - - with InvisiblePlaywright(seed=42, headless=True) as browser: - ctx = browser.new_context() - page = ctx.new_page() - page.goto("https://demo.fingerprint.com/playground") - validations: - required: true - - - type: textarea - id: expected - attributes: - label: What you expected - description: Most detectors will never give a perfect score for any browser. Tell us what threshold you'd accept (e.g. bot=not_detected, vm_ml_score < 0.3). - validations: - required: true - - - type: textarea - id: full_report - attributes: - label: Full detector response - description: For Fingerprint Pro paste the JSON from /api/event/v4/ if you have it. For CreepJS paste the full Smart Signals block. Optional but speeds things up a lot. - render: json - validations: - required: false - - - type: textarea - id: notes - attributes: - label: Notes - validations: - required: false - - - type: checkboxes - id: confirm - attributes: - label: Before submitting - options: - - label: Searched existing issues. - required: true - - label: On the latest released version. - required: true - - label: The detector verdict above is from a real run, not a hypothesis. - required: true - - label: Removed credentials, real IPs, FpJS visitor_id values, personal file paths from the snippet and full report. - required: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..805d579 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,79 @@ +name: Bug report +description: Report a bug in the invisible_playwright Python wrapper +title: "[bug] " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to file a bug report. + + Before continuing, please: + - Search [existing issues](https://github.com/feder-cr/invisible_playwright/issues?q=is%3Aissue) to avoid duplicates. + - If the bug is in the **patched Firefox itself** (canvas/WebGL/audio/font spoofing, a detector flagging the browser), open it at [feder-cr/firefox-stealth](https://github.com/feder-cr/firefox-stealth/issues) instead. + - **Do not** report security vulnerabilities here — follow [SECURITY.md](https://github.com/feder-cr/invisible_playwright/blob/main/SECURITY.md). + - type: input + id: version + attributes: + label: invisible_playwright version + description: Output of `invisible_playwright version` + placeholder: "0.1.0 (binary 150.0.1)" + validations: + required: true + - type: dropdown + id: os + attributes: + label: Operating system + options: + - Windows x86_64 + - Linux x86_64 + - Other (please specify in description) + validations: + required: true + - type: input + id: python + attributes: + label: Python version + placeholder: "3.11.7" + validations: + required: true + - type: textarea + id: repro + attributes: + label: Minimal reproduction + description: A small, self-contained code snippet that triggers the bug. Strip out anything unrelated. + render: python + validations: + required: true + - type: textarea + id: expected + attributes: + label: Expected behavior + validations: + required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + description: Include the full error message and traceback if any. + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs / additional context + description: Browser console output, environment variables, proxy config (redact credentials), etc. + render: text + validations: + required: false + - type: checkboxes + id: confirm + attributes: + label: Confirmations + options: + - label: I have searched existing issues and this bug has not been reported. + required: true + - label: I am on the latest release. + required: true + - label: I have removed any credentials, proxy passwords, or sensitive data from logs. + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 44f31be..6d3dace 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,9 +3,9 @@ contact_links: - name: Security vulnerability url: https://github.com/feder-cr/invisible_playwright/security/advisories/new about: Report a security issue privately. Do NOT open a public issue. - - name: Bug in the patched Firefox source (C++, IDL, Juggler JS) - url: https://github.com/feder-cr/invisible_firefox/issues - about: Source-level patches in the Firefox fork go in the invisible_firefox repo. Detection results (FpJS, CreepJS, etc.) use the stealth detection template here. + - name: Bug in the patched Firefox itself (canvas / WebGL / fonts / WebRTC / etc.) + url: https://github.com/feder-cr/firefox-stealth/issues + about: Spoofing/fingerprint bugs belong in the firefox-stealth repo. - name: Question or general discussion url: https://github.com/feder-cr/invisible_playwright/discussions - about: Usage questions, ideas, chat. Bugs and features still go in issues. + about: For usage questions, ideas, and chat. Bugs and features still go in issues. diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml deleted file mode 100644 index 0f750f8..0000000 --- a/.github/workflows/e2e.yml +++ /dev/null @@ -1,52 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# e2e.yml — run the FULL browser-driving e2e suite (the 127 @pytest.mark.e2e) -# on GitHub, on every push/PR to main. -# -# Why this can run on CI when the drive-gate had to stay light: the drive-gate -# launched Firefox in true HEADLESS mode, which is content-process unstable on -# the hosted runners (eval-CSP / context-destroyed). The stealth wrapper instead -# launches Firefox HEADED on a real display; under `xvfb-run` (a virtual X -# server) that's exactly what we get on a headless CI box — stable, and the same -# thing webrtc-e2e.yml already relies on. -# -# Secret-free, so it's safe in public CI: the binary is the PUBLIC firefox-9 -# release (no token), and the webrtc e2e fake a local TCP-only SOCKS. The proxy -# realness gate (fppro / smartproxy) is NOT here — it needs secrets and stays a -# local pre-release gate. -# ───────────────────────────────────────────────────────────────────────────── -name: e2e - -on: - push: - branches: [main] - pull_request: - branches: [main] - workflow_dispatch: - -permissions: - contents: read - -jobs: - e2e: - name: e2e (linux, xvfb) - runs-on: ubuntu-24.04 - timeout-minutes: 40 - steps: - - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: { fetch-depth: 1 } - - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 - with: { python-version: '3.11' } - - name: Install wrapper + test deps (+ pinned Playwright) - run: | - python -m pip install --upgrade pip - python -m pip install ".[dev]" - python -m pip install "playwright==$(cat scripts/playwright_pin.txt)" - - name: System deps (xvfb + Firefox runtime libs) - run: | - sudo apt-get update - sudo apt-get install -y xvfb - sudo "$(which python)" -m playwright install-deps firefox - - name: Fetch the published firefox binary - run: echo "FF=$(python -m invisible_playwright fetch | tail -1)" >> "$GITHUB_ENV" - - name: Run the full e2e suite under a virtual display - run: xvfb-run -a python scripts/run_e2e.py "$FF" diff --git a/.github/workflows/firefox-launch-matrix.yml b/.github/workflows/firefox-launch-matrix.yml deleted file mode 100644 index 4e7b053..0000000 --- a/.github/workflows/firefox-launch-matrix.yml +++ /dev/null @@ -1,106 +0,0 @@ -name: firefox-launch-matrix - -# Cross-Windows-edition smoke for the shipped firefox-N binary. -# Triggered by issue #22 (firefox-7 SxS mismatch on Win11 build 26200, -# reporter `jannusdorfer-create`). -# -# Runs the exact reporter snippet on every Windows runner GitHub offers, -# from a fresh checkout. If any matrix cell fails the same way, the bug -# is reproducible on at least one clean-ish environment and we ship a -# sidecar mozglue.manifest fix. If all cells pass, the bug is confined -# to the reporter's specific environment (Pro/Enterprise GPO, EDR, etc.). - -on: - workflow_dispatch: - push: - branches: [main] - paths: - - '.github/workflows/firefox-launch-matrix.yml' - -jobs: - smoke: - name: launch (${{ matrix.os }}, py${{ matrix.python }}) - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [windows-2022, windows-2025, windows-latest] - python: ["3.11", "3.12", "3.13"] - - steps: - - uses: actions/checkout@v4 - - - name: Set up Python ${{ matrix.python }} - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python }} - cache: pip - - - name: Windows edition + build info - shell: pwsh - run: | - $os = Get-CimInstance Win32_OperatingSystem - Write-Host "Caption : $($os.Caption)" - Write-Host "BuildNumber: $($os.BuildNumber)" - Write-Host "OSArch : $($os.OSArchitecture)" - Write-Host "Edition : $((Get-CimInstance Win32_OperatingSystem).OperatingSystemSKU)" - Write-Host "---" - Write-Host "VC++ Redistributables installed:" - Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*' ` - -ErrorAction SilentlyContinue | - Where-Object { $_.DisplayName -like '*Visual C++*Redist*' } | - Select-Object DisplayName, DisplayVersion | - Format-Table -AutoSize - - - name: Install package from this commit - run: | - python -m pip install --upgrade pip - pip install . - - - name: Fetch firefox-7 binary - run: python -m invisible_playwright fetch - - - name: Verify firefox.exe can launch standalone (the snippet that fails for issue #22) - shell: pwsh - run: | - # The platformdirs path has the duplicated `invisible-playwright` segment - # on Windows (user_cache_dir convention). - $ffPath = "$env:LOCALAPPDATA\invisible-playwright\invisible-playwright\Cache\firefox-7\firefox.exe" - if (-not (Test-Path $ffPath)) { - Write-Error "firefox.exe NOT FOUND at $ffPath" - exit 1 - } - Write-Host "Launching: $ffPath --version" - # NOTE: firefox.exe --version on Windows prints the version but may - # return non-zero exit code (sub-process fork quirk). Check stdout. - $output = & $ffPath --version 2>&1 | Out-String - Write-Host "Output: $output" - if ($output -notmatch 'Mozilla Firefox \d') { - Write-Error "firefox.exe --version did not print a Mozilla Firefox version. Output was: $output" - exit 1 - } - Write-Host "OK: firefox.exe runs and prints version." - - - name: Run reporter's exact InvisiblePlaywright snippet - run: | - python -c " - import asyncio - from invisible_playwright.async_api import InvisiblePlaywright - async def main(): - async with InvisiblePlaywright(seed=9128) as browser: - page = await browser.new_page() - await page.goto('about:blank') - print('OK: page loaded, url =', page.url) - asyncio.run(main()) - " - - - name: Upload diagnostics on failure - if: failure() - uses: actions/upload-artifact@v4 - with: - name: launch-failure-${{ matrix.os }}-py${{ matrix.python }} - path: | - ${{ env.LOCALAPPDATA }}/invisible-playwright/invisible-playwright/Cache/firefox-7/firefox.exe - ${{ env.LOCALAPPDATA }}/invisible-playwright/invisible-playwright/Cache/firefox-7/mozglue.dll - if-no-files-found: warn - retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index a8e0d14..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,479 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# release.yml — build all 5 patched-Firefox targets at $0 and publish them as -# DRAFT GitHub Release assets, named per the wrapper contract (constants.ARCHIVE_NAME). -# DRAFT on purpose: a human runs the realness gate and only THEN un-drafts + bumps -# BINARY_VERSION. Nothing auto-ships (issue #14 lesson). -# -# PACKAGING (issue #14: dangling symlinks broke 265 downloads — never again): -# Linux → cp -aL (dereference ALL symlinks into real files) + rm dev tools + -# strip + sanitize + tar at ROOT, then validate_release.py as a HARD -# in-pipeline gate (the exact battle-tested script from the source repo). -# Win → mach package; zip the CONTENTS of dist/firefox (clean tree, NOT -# dist/bin) so firefox.exe sits at the zip ROOT. -# macOS → mach package; ad-hoc codesign the .app; PRESERVE its internal relative -# symlinks (a .app legitimately has them — cp -aL would break it); verify -# every symlink is relative+internal; tar the bundle. --version self-gate. -# -# DRIVE GATE (the firefox-8 catcher): after build, every binary is DRIVEN by -# Playwright on its native runner (launch via juggler + real page + JS roundtrip, -# headless, no screenshot → GPU-free, zero proxy). A juggler-less binary renders -# a screenshot fine but is undrivable — only an actual drive catches that. The -# proxy realness gate (fppro/webrtc) stays LOCAL — it needs secrets. -# -# Trigger: push a tag `firefox-N`, or run manually. Hybrid runners, all free. -# ───────────────────────────────────────────────────────────────────────────── -name: release - -on: - push: - tags: ['firefox-*'] - workflow_dispatch: - inputs: - source_ref: - description: 'invisible_firefox ref to build' - default: 'stealth/150' - release_tag: - description: 'release tag to publish the draft under (e.g. firefox-9)' - required: true - -env: - SOURCE_REPO: feder-cr/invisible_firefox - SOURCE_REF: ${{ github.event.inputs.source_ref || 'stealth/150' }} - -jobs: - build: - name: build-${{ matrix.leg }} - runs-on: ${{ matrix.runner }} - timeout-minutes: 350 - strategy: - fail-fast: false - matrix: - include: - - leg: linux-x86_64 - runner: ubuntu-24.04 - family: linux - target: '' - rust_target: x86_64-unknown-linux-gnu - win_disables: 'no' - extra_pkgs: '' - asset: firefox-150.0.1-stealth-linux-x86_64.tar.gz - - leg: linux-arm64 - runner: ubuntu-24.04-arm - family: linux - target: '' - rust_target: aarch64-unknown-linux-gnu - win_disables: 'no' - extra_pkgs: '' - asset: firefox-150.0.1-stealth-linux-arm64.tar.gz - - leg: win-x86_64 - runner: ubuntu-24.04 - family: win - target: x86_64-pc-windows-msvc - rust_target: x86_64-pc-windows-msvc - win_disables: 'yes' - extra_pkgs: 'msitools p7zip-full zip' - asset: firefox-150.0.1-stealth-win-x86_64.zip - - leg: macos-arm64 - runner: macos-15 - family: mac - target: aarch64-apple-darwin - rust_target: aarch64-apple-darwin - win_disables: 'no' - extra_pkgs: '' - asset: firefox-150.0.1-stealth-macos-arm64.tar.gz - - leg: macos-x86_64 - runner: macos-15-intel - family: mac - target: x86_64-apple-darwin - rust_target: x86_64-apple-darwin - win_disables: 'no' - extra_pkgs: '' - asset: firefox-150.0.1-stealth-macos-x86_64.tar.gz - steps: - - name: Free disk + 16G swap (Linux runners) - if: matrix.family != 'mac' - run: | - sudo rm -rf /usr/share/dotnet /opt/ghc /usr/local/lib/android \ - /usr/local/share/boost "${AGENT_TOOLSDIRECTORY:-/opt/hostedtoolcache}" 2>/dev/null || true - sudo fallocate -l 16G /swapfile && sudo chmod 600 /swapfile && sudo mkswap /swapfile && sudo swapon /swapfile || true - - - name: Checkout patched Firefox source - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: - repository: ${{ env.SOURCE_REPO }} - ref: ${{ env.SOURCE_REF }} - fetch-depth: 1 - - # Record which invisible_firefox commit this build came from. The publish - # job turns the range previous-release..this commit into the release notes - # (scripts/gen_release_notes.py), and re-publishes it as a source-commit.txt - # asset so the NEXT release knows where to start the changelog. One leg is - # enough — all legs check out the same SOURCE_REF. - - name: Record source commit (for auto release notes) - if: matrix.leg == 'linux-x86_64' - shell: bash - run: git rev-parse HEAD > source-commit.txt && cat source-commit.txt - - name: Upload source-commit artifact - if: matrix.leg == 'linux-x86_64' - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: source-commit - path: source-commit.txt - if-no-files-found: error - retention-days: 7 - - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 - with: { python-version: '3.11' } - - - name: Install Linux build tools - if: matrix.family != 'mac' - run: | - sudo apt-get update - sudo apt-get install -y util-linux binutils ${{ matrix.extra_pkgs }} - - - name: Select Xcode 26.2 + export SDK path (macOS) - if: matrix.family == 'mac' - run: | - sudo xcode-select -s /Applications/Xcode_26.2.app - SDKP="$(xcrun --show-sdk-path)" - echo "SDK_PATH=$SDKP" >> "$GITHUB_ENV" - echo "macOS SDK $(xcrun --sdk macosx --show-sdk-version) at $SDKP" - - - name: Add Rust target - run: rustup target add ${{ matrix.rust_target }} || true - - - name: Extend the repo .mozconfig (NO mold; +target/SDK as needed) - run: | - test -f .mozconfig || { echo "ERROR: no .mozconfig in source"; exit 1; } - rm -f mozconfig - { - echo "" - echo "# --- release CI levers for ${{ matrix.leg }} (mold intentionally OFF — it segfaults libxul) ---" - echo "ac_add_options --disable-debug-symbols" - } >> .mozconfig - if [ -n "${{ matrix.target }}" ]; then echo "ac_add_options --target=${{ matrix.target }}" >> .mozconfig; fi - if [ "${{ matrix.family }}" = "mac" ]; then echo "ac_add_options --with-macos-sdk=$SDK_PATH" >> .mozconfig; fi - if [ "${{ matrix.win_disables }}" = "yes" ]; then - { echo "ac_add_options --disable-default-browser-agent"; - echo "ac_add_options --disable-maintenance-service"; - echo "ac_add_options --disable-update-agent"; } >> .mozconfig - fi - if [ "${{ matrix.family }}" = "mac" ]; then NCPU=$(sysctl -n hw.ncpu); else NCPU=4; fi - { echo "mk_add_options MOZ_PARALLEL_BUILD=$NCPU"; - echo "mk_add_options MOZ_OBJDIR=@TOPSRCDIR@/obj-rel"; } >> .mozconfig - echo "----- final .mozconfig -----"; cat .mozconfig - - - name: Build - run: ./mach build - - # ── LINUX: dereference symlinks (issue #14) + strip + sanitize + tar@root + GATE - - name: Package + validate (Linux) - if: matrix.family == 'linux' - run: | - set -e - DIST=obj-rel/dist/bin - STAGING=staging - rm -rf "$STAGING"; mkdir -p "$STAGING" out - cp -aL "$DIST/." "$STAGING/" # -L: dereference ALL symlinks into real files - N=$(find "$STAGING" -type l | wc -l) - [ "$N" -eq 0 ] || { echo "ERROR: $N symlinks remain after cp -aL"; exit 1; } - for t in xpcshell certutil pk12util rapl; do rm -f "$STAGING/$t"; done - # JUGGLER GATE: the binary is undrivable by Playwright without it (see 70-known-bugs) - { [ -e "$STAGING/chrome/juggler.manifest" ] && [ -d "$STAGING/chrome/juggler" ]; } \ - || { echo "ERROR: juggler missing from package (chrome/juggler) — Playwright can't drive it"; exit 1; } - echo "juggler GATE OK (loose chrome/juggler present)" - find "$STAGING" -type f \ - \( -name '*.so' -o -name firefox -o -name firefox-bin -o -name plugin-container \ - -o -name pingsender -o -name glxtest -o -name vaapitest -o -name updater \) \ - -exec strip --strip-debug {} + 2>/dev/null || true - STAGING="$STAGING" python3 scripts/linux_sanitize.py || true # no-op in CI (no /home/feder), defensive - tar --owner=0 --group=0 --numeric-owner --mtime="2026-01-01 00:00:00 UTC" \ - -czf "out/${{ matrix.asset }}" -C "$STAGING" . # firefox at ROOT - echo "=== HARD GATE: scripts/validate_release.py (the issue-#14 protector) ===" - python3 scripts/validate_release.py --linux "out/${{ matrix.asset }}" --linux-only - ls -la out/ - - # ── WINDOWS (cross): zip the CLEAN dist/firefox tree, firefox.exe at root - - name: Package (Windows cross) - if: matrix.family == 'win' - run: | - set -e - # Do NOT swallow a mach failure: `./mach package || echo` lets set -e pass - # and would fall through to a stale tree. A release MUST come from the clean - # dist/firefox; dist/bin is the dev tree (cruft + loose juggler that masked - # the firefox-7/8 packaging bugs), never acceptable for a release. - ./mach package - [ -f obj-rel/dist/firefox/firefox.exe ] \ - || { echo "ERROR: mach package did not produce a clean dist/firefox tree"; exit 1; } - WIN_APP=obj-rel/dist/firefox - echo "packaging from: $WIN_APP" - # JUGGLER GATE: omni.ja must carry juggler (else Playwright can't drive it) - [ -f "$WIN_APP/omni.ja" ] || { echo "ERROR: no omni.ja in $WIN_APP"; exit 1; } - python3 -c "import zipfile,sys; sys.exit(0 if any('juggler' in n.lower() for n in zipfile.ZipFile('$WIN_APP/omni.ja').namelist()) else 1)" \ - || { echo "ERROR: juggler missing from $WIN_APP/omni.ja — Playwright can't drive it"; exit 1; } - echo "juggler GATE OK (win)" - mkdir -p out - ( cd "$WIN_APP" && zip -qr "$GITHUB_WORKSPACE/out/${{ matrix.asset }}" . ) # firefox.exe at zip ROOT - ls -la out/ - - # ── macOS: package .app, ad-hoc sign, verify relative-internal symlinks, --version gate, tar - - name: Package + validate (macOS) - if: matrix.family == 'mac' - run: | - set -e - ./mach package - APP="$(find obj-rel/dist -maxdepth 2 -name '*.app' -type d | head -1)" - [ -n "$APP" ] || { echo "ERROR: no .app produced"; exit 1; } - echo "built app: $APP" - # JUGGLER GATE: the .app's omni.ja must carry juggler (else Playwright can't drive it) - python3 -c "import zipfile,sys,glob; jas=glob.glob('$APP/Contents/Resources/omni.ja')+glob.glob('$APP/Contents/Resources/browser/omni.ja'); sys.exit(0 if jas and any(any('juggler' in n.lower() for n in zipfile.ZipFile(j).namelist()) for j in jas) else 1)" \ - || { echo "ERROR: juggler missing from .app omni.ja — Playwright can't drive it"; exit 1; } - echo "juggler GATE OK (mac)" - codesign --force --deep --sign - --timestamp=none "$APP" - codesign --verify --deep --strict --verbose=2 "$APP" - echo "=== --version GATE ===" - "$APP/Contents/MacOS/firefox" --version - echo "=== critical files present ===" - for need in "Contents/MacOS/firefox" "Contents/Info.plist"; do - [ -e "$APP/$need" ] || { echo "ERROR: missing $need"; exit 1; } - done - echo "=== Info.plist well-formed + required keys (a malformed plist → Finder 'damaged') ===" - plutil -lint "$APP/Contents/Info.plist" - for key in CFBundleExecutable CFBundleIdentifier CFBundleShortVersionString; do - plutil -extract "$key" raw -o - "$APP/Contents/Info.plist" >/dev/null \ - || { echo "ERROR: Info.plist missing $key"; exit 1; } - done - EXEC="$(plutil -extract CFBundleExecutable raw -o - "$APP/Contents/Info.plist")" - [ -e "$APP/Contents/MacOS/$EXEC" ] \ - || { echo "ERROR: CFBundleExecutable '$EXEC' has no matching binary in Contents/MacOS"; exit 1; } - echo "=== verify NO absolute symlinks in the .app (relative-internal ones are fine) ===" - BAD="$(find "$APP" -type l -print0 | xargs -0 -I{} sh -c 't=$(readlink "{}"); case "$t" in /*) echo "{} -> $t";; esac')" - [ -z "$BAD" ] || { echo "ERROR: absolute symlinks in .app (break on user machines):"; echo "$BAD" | head -5; exit 1; } - echo "mac .app OK: critical files present, no absolute symlinks" - STABLE="$(dirname "$APP")/Firefox.app" - [ "$APP" = "$STABLE" ] || mv "$APP" "$STABLE" - mkdir -p out - tar -czf "out/${{ matrix.asset }}" -C "$(dirname "$STABLE")" Firefox.app # preserves internal symlinks - ls -la out/ - - - name: Upload build artifact - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 - with: - name: asset-${{ matrix.leg }} - path: out/${{ matrix.asset }} - if-no-files-found: error - retention-days: 7 - - # DRIVE GATE — the firefox-8 catcher. A raw `firefox --screenshot` proves - # nothing about automation: a juggler-less binary renders fine and ships - # broken (firefox-8 did exactly that). So we DRIVE every binary the way users - # will: Playwright launches it over the juggler pipe, loads a real page, and - # round-trips JS. A binary missing/broken juggler throws TargetClosedError - # here and the release never publishes. Headless, NO screenshot → GPU-free, - # so it can't false-fail on the GPU-less hosted runners. Zero proxy / zero - # secrets → safe in public CI (the proxy realness gate stays local, by design). - # Each leg runs on its NATIVE runner so we test the real artifact, not a cross - # surrogate. Playwright is pinned to a version validated against this build's - # juggler; bump it in lockstep when the juggler is re-synced from upstream. - gate: - name: gate-${{ matrix.leg }} - needs: build - runs-on: ${{ matrix.runner }} - timeout-minutes: 25 - strategy: - fail-fast: false - matrix: - include: - # `extra: --full` adds the mouse/keyboard/canvas/navsurface interaction - # checks. Only on linux-x86_64 (historically the most reliable hosted - # runner): the interaction code is platform-identical JS (omni.ja), so - # one reliable full run catches a firefox-2-class regression for all - # platforms. The other legs run SMOKE (launch+http+UA+webdriver) — the - # firefox-8/juggler catcher — which is robust even on the flaky - # windows-latest runner. See scripts/ci_drive_gate.py. - - leg: linux-x86_64 - runner: ubuntu-24.04 - kind: linux - asset: firefox-150.0.1-stealth-linux-x86_64.tar.gz - extra: '--full' - - leg: linux-arm64 - runner: ubuntu-24.04-arm - kind: linux - asset: firefox-150.0.1-stealth-linux-arm64.tar.gz - extra: '' - - leg: win-x86_64 - runner: windows-latest - kind: win - asset: firefox-150.0.1-stealth-win-x86_64.zip - extra: '' - - leg: macos-arm64 - runner: macos-15 - kind: mac - asset: firefox-150.0.1-stealth-macos-arm64.tar.gz - extra: '' - - leg: macos-x86_64 - runner: macos-15-intel - kind: mac - asset: firefox-150.0.1-stealth-macos-x86_64.tar.gz - extra: '' - steps: - - name: Checkout wrapper (for scripts/ci_drive_gate.py) - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: { fetch-depth: 1 } - - name: Download asset - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - name: asset-${{ matrix.leg }} - path: art - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 - with: { python-version: '3.11' } - - name: Install Playwright driver (no bundled browser — we override executable_path) - # Pin from a SINGLE source (scripts/playwright_pin.txt) so release.yml and - # verify-assets.yml can't drift to different versions. The drive gate then - # ENFORCES playwright↔juggler compatibility: an incompatible pin fails the - # launch/drive (TargetClosedError / protocol error) and nothing publishes. - # Bump the pin file in lockstep when the juggler is re-synced from upstream. - shell: bash - run: python -m pip install --quiet "playwright==$(cat scripts/playwright_pin.txt)" - - name: Linux system deps for headless firefox - if: matrix.kind == 'linux' - run: sudo "$(which python)" -m playwright install-deps firefox - - name: Extract + locate firefox binary - shell: bash - run: | - set -e - mkdir -p ff - A="art/${{ matrix.asset }}" - case "${{ matrix.kind }}" in - win) python -c "import zipfile; zipfile.ZipFile('$A').extractall('ff')"; EXE="ff/firefox.exe";; - linux) tar xzf "$A" -C ff; EXE="ff/firefox";; - mac) tar xzf "$A" -C ff; EXE="ff/Firefox.app/Contents/MacOS/firefox";; - esac - [ -e "$EXE" ] || { echo "ERROR: firefox binary not found at $EXE"; exit 1; } - chmod +x "$EXE" 2>/dev/null || true - echo "FF_EXE=$EXE" >> "$GITHUB_ENV" - echo "located: $EXE" - - name: DRIVE GATE — Playwright launch via juggler + real page (+ interaction on --full) - shell: bash - run: python scripts/ci_drive_gate.py "$FF_EXE" ${{ matrix.extra }} - - # CLOAK + WEBGL-MASKING GUARDS — run the wrapper's e2e cloak/gamma checks - # against THIS leg's freshly-built artifact, on its native runner. The - # wrapper's headless=True is headed+hidden (cloak on Win/macOS, its own - # Xvfb on Linux). Linux (Xvfb + llvmpipe) and Windows (WARP) give a - # software WebGL context on the GPU-less hosts, so the WebGL-dependent - # assertions run there. macOS GitHub runners expose NO WebGL in the CI - # session at all (even vanilla Firefox; macOS has no software-GL fallback), - # so on the mac legs the WebGL checks self-skip and the cloak is validated - # via its non-blank screenshot + CGWindowAlpha == 0. test_cloak asserts the - # window is hidden (Windows DWMWA_CLOAKED / macOS CGWindowAlpha) AND still - # renders — the macOS leg is the only place the cocoa cloak patch gets RUN. - # The webgl guard catches a regression of the gamma readPixels noise back to - # the pixelscan-maskable ±1 spike form (covered on Linux + Windows). - - name: Install pyobjc Quartz (macOS — to read the cloak window alpha) - if: matrix.kind == 'mac' - run: python -m pip install --quiet pyobjc-framework-Quartz - - name: Cloak + WebGL-masking guards (headed) - shell: bash - run: | - python -m pip install --quiet ".[dev]" - INVPW_BINARY_PATH="$FF_EXE" python -m pytest \ - tests/test_cloak.py \ - "tests/test_fingerprint_surface.py::test_webgl_readpixels_no_masking_signature" \ - -m e2e -o addopts='' -q - - publish: - name: publish-draft-release - needs: [build, gate] - runs-on: ubuntu-24.04 - permissions: - contents: write - steps: - - name: Checkout wrapper (for scripts/gen_release_notes.py) - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: { fetch-depth: 1 } - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 - with: { python-version: '3.11' } - - name: Download all build assets - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: { pattern: asset-*, path: dl, merge-multiple: true } - - name: Download source-commit metadata - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: { name: source-commit, path: src-meta } - - name: Assert all 5 target archives present (no silent partial release) - run: | - cd dl - EXPECTED=" - firefox-150.0.1-stealth-linux-x86_64.tar.gz - firefox-150.0.1-stealth-linux-arm64.tar.gz - firefox-150.0.1-stealth-win-x86_64.zip - firefox-150.0.1-stealth-macos-arm64.tar.gz - firefox-150.0.1-stealth-macos-x86_64.tar.gz - " - for a in $EXPECTED; do - [ -s "$a" ] || { echo "ERROR: missing/empty release asset: $a (a build leg silently dropped out?)"; exit 1; } - done - echo "all 5 target archives present" - - name: Generate checksums.txt - run: | - cd dl; ls -la - # explicit glob — never include checksums.txt itself (the `*`-includes-itself trap) - sha256sum firefox-150.0.1-stealth-* > checksums.txt - echo "----- checksums.txt -----"; cat checksums.txt - - name: Resolve release tag - id: tag - run: | - TAG="${{ github.event.inputs.release_tag }}" - [ -z "$TAG" ] && TAG="${GITHUB_REF_NAME}" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - # bare revision number for the release title: firefox-10 -> 10 - N="${TAG#firefox-}" - echo "num=$N" >> "$GITHUB_OUTPUT" - # previous release tag, for the changelog range (firefox-10 -> firefox-9) - case "$N" in (*[!0-9]*|'') echo "prevtag=" >> "$GITHUB_OUTPUT";; - (*) echo "prevtag=firefox-$((N-1))" >> "$GITHUB_OUTPUT";; esac - echo "publishing DRAFT release for tag: $TAG" - - name: Build release notes from the source commits - id: notes - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -e - CUR="$(cat src-meta/source-commit.txt 2>/dev/null | tr -d '[:space:]')" - echo "this build's source commit: ${CUR:-}" - # previous release's recorded source commit — gives the changelog range. - # Missing (first automated notes / firefox-0) -> notes omit the changelog. - PREV="" - PREVTAG="${{ steps.tag.outputs.prevtag }}" - if [ -n "$PREVTAG" ] && gh release download "$PREVTAG" -R "${{ github.repository }}" \ - --pattern source-commit.txt --dir prev 2>/dev/null; then - PREV="$(cat prev/source-commit.txt | tr -d '[:space:]')" - echo "previous ($PREVTAG) source commit: $PREV" - else - echo "no previous source-commit.txt — changelog section omitted this time" - fi - python scripts/gen_release_notes.py --tag "${{ steps.tag.outputs.tag }}" \ - --current "$CUR" --prev-sha "$PREV" --source-repo "${{ env.SOURCE_REPO }}" > body.md - echo "----- generated body.md -----"; cat body.md - # publish THIS build's source commit so the next release can diff from it - cp src-meta/source-commit.txt dl/source-commit.txt - - name: Create DRAFT release with all assets - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 - with: - tag_name: ${{ steps.tag.outputs.tag }} - name: invisible_firefox (150.0.1) rev ${{ steps.tag.outputs.num }} - draft: true - prerelease: false - fail_on_unmatched_files: true - files: | - dl/*.tar.gz - dl/*.zip - dl/checksums.txt - dl/source-commit.txt - body_path: body.md - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/verify-assets.yml b/.github/workflows/verify-assets.yml deleted file mode 100644 index b4d3567..0000000 --- a/.github/workflows/verify-assets.yml +++ /dev/null @@ -1,111 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# verify-assets.yml — re-runnable DRIVE GATE for an EXISTING release's assets. -# -# release.yml drive-gates every binary it builds. This does the same drive test -# WITHOUT rebuilding: it downloads a release's already-published assets (works on -# DRAFT releases too via GITHUB_TOKEN) and drives each one on its native runner. -# -# Use it to: -# • drive-test a release that was built before the in-pipeline gate existed -# (e.g. firefox-9, built on the old release.yml), or -# • re-verify any shipped release on demand (regression check). -# -# Same single-source-of-truth drive logic as release.yml: scripts/ci_drive_gate.py. -# Headless, no screenshot → GPU-free. Zero proxy / zero secrets. -# ───────────────────────────────────────────────────────────────────────────── -name: verify-assets - -on: - workflow_dispatch: - inputs: - release_tag: - description: 'release tag whose assets to drive-test (e.g. firefox-9)' - required: true - -permissions: - # write (not read) is required: GitHub only exposes DRAFT releases to tokens - # with push access. With contents:read, `gh release download` on a draft tag - # 404s ("release not found"). This workflow only READS assets — the elevated - # scope is solely to make draft releases visible to GITHUB_TOKEN. - contents: write - -jobs: - drive: - name: drive-${{ matrix.leg }} - runs-on: ${{ matrix.runner }} - timeout-minutes: 25 - strategy: - fail-fast: false - matrix: - include: - # --full (interaction) only on the reliable linux-x86_64 leg; others run - # the robust SMOKE drive. Same rationale as release.yml's gate. - - leg: linux-x86_64 - runner: ubuntu-24.04 - kind: linux - asset: firefox-150.0.1-stealth-linux-x86_64.tar.gz - extra: '--full' - - leg: linux-arm64 - runner: ubuntu-24.04-arm - kind: linux - asset: firefox-150.0.1-stealth-linux-arm64.tar.gz - extra: '' - - leg: win-x86_64 - runner: windows-latest - kind: win - asset: firefox-150.0.1-stealth-win-x86_64.zip - extra: '' - - leg: macos-arm64 - runner: macos-15 - kind: mac - asset: firefox-150.0.1-stealth-macos-arm64.tar.gz - extra: '' - - leg: macos-x86_64 - runner: macos-15-intel - kind: mac - asset: firefox-150.0.1-stealth-macos-x86_64.tar.gz - extra: '' - steps: - - name: Checkout wrapper (for scripts/ci_drive_gate.py) - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: { fetch-depth: 1 } - - name: Download the release asset (draft releases included) - shell: bash - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -e - mkdir -p art - gh release download "${{ github.event.inputs.release_tag }}" \ - --repo "${{ github.repository }}" \ - --pattern "${{ matrix.asset }}" \ - --dir art - ls -la art/ - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 - with: { python-version: '3.11' } - - name: Install Playwright driver (no bundled browser — we override executable_path) - # Single-source pin (see release.yml); the drive gate enforces juggler compat. - shell: bash - run: python -m pip install --quiet "playwright==$(cat scripts/playwright_pin.txt)" - - name: Linux system deps for headless firefox - if: matrix.kind == 'linux' - run: sudo "$(which python)" -m playwright install-deps firefox - - name: Extract + locate firefox binary - shell: bash - run: | - set -e - mkdir -p ff - A="art/${{ matrix.asset }}" - case "${{ matrix.kind }}" in - win) python -c "import zipfile; zipfile.ZipFile('$A').extractall('ff')"; EXE="ff/firefox.exe";; - linux) tar xzf "$A" -C ff; EXE="ff/firefox";; - mac) tar xzf "$A" -C ff; EXE="ff/Firefox.app/Contents/MacOS/firefox";; - esac - [ -e "$EXE" ] || { echo "ERROR: firefox binary not found at $EXE"; exit 1; } - chmod +x "$EXE" 2>/dev/null || true - echo "FF_EXE=$EXE" >> "$GITHUB_ENV" - echo "located: $EXE" - - name: DRIVE GATE — Playwright launch via juggler + real page (+ interaction on --full) - shell: bash - run: python scripts/ci_drive_gate.py "$FF_EXE" ${{ matrix.extra }} diff --git a/.github/workflows/verify-cloak.yml b/.github/workflows/verify-cloak.yml deleted file mode 100644 index 4d119e8..0000000 --- a/.github/workflows/verify-cloak.yml +++ /dev/null @@ -1,103 +0,0 @@ -# ───────────────────────────────────────────────────────────────────────────── -# verify-cloak.yml — re-runnable CLOAK + WEBGL-MASKING GUARDS for an EXISTING -# build run's artifacts, WITHOUT rebuilding Firefox (~3h on the mac legs). -# -# release.yml runs these same guards in its `gate` job against each freshly-built -# artifact. This re-runs them against the artifacts of a PRIOR build run (input -# `run_id`) using the CURRENT wrapper code on the default branch — so a test-only -# fix (e.g. making the macOS leg tolerant of the runner's missing WebGL) can be -# validated against the real binaries in ~10 min instead of paying a full rebuild. -# -# Same guard command as release.yml's gate. Headed-but-cloaked; zero proxy / zero -# secrets. The macOS legs are the only place the cocoa cloak patch actually RUNS. -# ───────────────────────────────────────────────────────────────────────────── -name: verify-cloak - -on: - workflow_dispatch: - inputs: - run_id: - description: 'build run id whose asset-* artifacts to re-gate (e.g. 27346856197)' - required: true - -permissions: - contents: read - actions: read # download-artifact needs this to read another run's artifacts - -jobs: - guard: - name: guard-${{ matrix.leg }} - runs-on: ${{ matrix.runner }} - timeout-minutes: 25 - strategy: - fail-fast: false - matrix: - # Same legs/runners/assets as release.yml's gate matrix. - include: - - leg: linux-x86_64 - runner: ubuntu-24.04 - kind: linux - asset: firefox-150.0.1-stealth-linux-x86_64.tar.gz - - leg: linux-arm64 - runner: ubuntu-24.04-arm - kind: linux - asset: firefox-150.0.1-stealth-linux-arm64.tar.gz - - leg: win-x86_64 - runner: windows-latest - kind: win - asset: firefox-150.0.1-stealth-win-x86_64.zip - - leg: macos-arm64 - runner: macos-15 - kind: mac - asset: firefox-150.0.1-stealth-macos-arm64.tar.gz - - leg: macos-x86_64 - runner: macos-15-intel - kind: mac - asset: firefox-150.0.1-stealth-macos-x86_64.tar.gz - steps: - - name: Checkout wrapper (current default branch — the FIXED tests) - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - with: { fetch-depth: 1 } - - name: Download build asset from the prior run (no rebuild) - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 - with: - name: asset-${{ matrix.leg }} - path: art - run-id: ${{ github.event.inputs.run_id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - name: Set up Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 - with: { python-version: '3.11' } - - name: Install Playwright driver (no bundled browser — we override executable_path) - # Single-source pin (see release.yml); the wrapper enforces juggler compat. - shell: bash - run: python -m pip install --quiet "playwright==$(cat scripts/playwright_pin.txt)" - - name: Linux system deps for headless firefox - if: matrix.kind == 'linux' - run: sudo "$(which python)" -m playwright install-deps firefox - - name: Extract + locate firefox binary - shell: bash - run: | - set -e - mkdir -p ff - A="art/${{ matrix.asset }}" - case "${{ matrix.kind }}" in - win) python -c "import zipfile; zipfile.ZipFile('$A').extractall('ff')"; EXE="ff/firefox.exe";; - linux) tar xzf "$A" -C ff; EXE="ff/firefox";; - mac) tar xzf "$A" -C ff; EXE="ff/Firefox.app/Contents/MacOS/firefox";; - esac - [ -e "$EXE" ] || { echo "ERROR: firefox binary not found at $EXE"; exit 1; } - chmod +x "$EXE" 2>/dev/null || true - echo "FF_EXE=$EXE" >> "$GITHUB_ENV" - echo "located: $EXE" - - name: Install pyobjc Quartz (macOS — to read the cloak window alpha) - if: matrix.kind == 'mac' - run: python -m pip install --quiet pyobjc-framework-Quartz - - name: Cloak + WebGL-masking guards (headed) - shell: bash - run: | - python -m pip install --quiet ".[dev]" - INVPW_BINARY_PATH="$FF_EXE" python -m pytest \ - tests/test_cloak.py \ - "tests/test_fingerprint_surface.py::test_webgl_readpixels_no_masking_signature" \ - -m e2e -o addopts='' -q diff --git a/.github/workflows/webrtc-e2e.yml b/.github/workflows/webrtc-e2e.yml deleted file mode 100644 index d14b8ce..0000000 --- a/.github/workflows/webrtc-e2e.yml +++ /dev/null @@ -1,47 +0,0 @@ -name: webrtc-e2e - -# Live WebRTC realness check against the shipped patched binary. -# -# Manual (workflow_dispatch) on purpose: it needs a firefox-N binary that -# carries the WebRTC fixes (synthetic srflx in genuine nICEr form + the -# default-route fallback behind a proxy). Run it after publishing such a -# binary — it is the release gate for "WebRTC looks real behind a proxy". -# Until that binary ships, test_not_blocked_behind_tcp_only_socks is EXPECTED -# to fail (the old binary is fully blocked behind a SOCKS proxy), which is the -# whole point of the gate. -# -# No smartproxy / credentials: the "behind a proxy" condition is faked by an -# in-process TCP-only SOCKS5 server (refuses UDP ASSOCIATE) and the egress IP -# is injected as an RFC 5737 TEST-NET address. Fully self-contained. - -on: - workflow_dispatch: - -jobs: - webrtc-e2e: - name: webrtc realness (ubuntu, py3.12) - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Python 3.12 - uses: actions/setup-python@v5 - with: - python-version: "3.12" - cache: pip - - - name: Install package + dev extras - run: | - python -m pip install --upgrade pip - pip install -e ".[dev]" - - - name: Fetch the patched Firefox binary - run: python -m invisible_playwright fetch - - - name: Resolve binary path - run: echo "STEALTHFOX_E2E_BINARY=$(python -m invisible_playwright path)" >> "$GITHUB_ENV" - - - name: Run WebRTC realness e2e (xvfb for the headless Firefox) - run: | - sudo apt-get update && sudo apt-get install -y xvfb - xvfb-run -a pytest tests/test_webrtc_realness.py -m e2e -o addopts="" -v -rs diff --git a/CHANGELOG.md b/CHANGELOG.md index f142d90..e9d5aec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,57 +6,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [Unreleased] -### Added -- `timezone="auto"`: the browser timezone is auto-derived from the egress IP. By default (no explicit timezone) it ALWAYS resolves — from the proxy egress when a proxy is set, otherwise from the host's own public IP — so the zone can never disagree with the IP (the classic `timezone_mismatch` signal). An explicit `"Area/City"` is the only way to force a specific zone. On failure: with a proxy the launch raises (no silent host-TZ fallback behind a foreign proxy); without a proxy it falls back to the host TZ so a transient lookup can't break the launch. -- The egress IP is mapped to its IANA zone with an offline mmdb (`daijro/geoip-all-in-one`). It auto-updates against the upstream weekly rebuild: cached locally, re-checked after `GEOIP_REFRESH_DAYS` (7), older copies pruned, and a stale cache is reused when offline. `STEALTHFOX_GEOIP_MMDB` points at your own `.mmdb` to skip the download. -- `resolve_session_timezone(timezone, proxy)` and `ensure_geoip_mmdb()` re-exported at the package root (plus `GeoTimezoneError`) so integrations that own their launch can reproduce the resolution. -- `tests/test_geo.py` (37) + `tests/test_geoip_update.py` (freshness / auto-update / offline fallback) unit tests. - -### Changed -- New runtime dependencies: `requests[socks]` (SOCKS egress lookup), `maxminddb` (mmdb reader), `tzdata` (IANA database for `zoneinfo`, which Windows lacks). - -## [0.2.0] - 2026-05-28 - -### Added -- Public config helpers in `invisible_playwright.config`: `get_default_stealth_prefs(seed, *, pin, locale, timezone, extra_prefs, humanize, virtual_display)` returns a complete `firefox_user_prefs` dict; `get_default_args()` returns the baseline CLI args list (currently empty). Both also re-exported at the package root. -- `invisible_playwright.ensure_binary` re-exported at the package root for parity with the `cloakbrowser.download.ensure_binary` integration pattern that downstream projects (Skyvern, Crawlee, agno) already expect. -- These helpers let third-party fetchers (changedetection.io plugins, Crawlee `BrowserPool` subclasses, agno toolkits) drive `playwright.firefox.launch(executable_path=..., firefox_user_prefs=...)` themselves without depending on the `InvisiblePlaywright` context manager owning the lifecycle. -- `tests/unit/test_config_public.py`: 14 unit tests covering deterministic seed, locale / timezone / pin / extra_prefs / humanize variations, and round-trip via the public namespace. - -### Unchanged -- `InvisiblePlaywright` context manager surface is identical (backwards compatible). -- `BINARY_VERSION` stays at `firefox-7`. Python-only release; no new Firefox build. - -## [0.1.8] - 2026-05-23 - -### Fixed -- [#20](https://github.com/feder-cr/invisible_playwright/issues/20): cross-origin iframes were unreachable from Playwright. `element_handle.content_frame()` returned `None`, `frame.evaluate()` threw cross-origin SOP errors, and `frame_locator(...).click()` timed out even with `force=True`. Root cause: FF150 defaults `fission.webContentIsolationStrategy=1` (`IsolateEverything`), which site-isolates every cross-origin iframe into a separate `webIsolated` content process even when `fission.autostart=False`. The parent's Juggler FrameTree then has a Frame placeholder with no docShell and no URL — every protocol op that needs to enter the iframe fails. Fix: pin `fission.webContentIsolationStrategy=0` (`IsolateNothing`) in the baseline prefs. The setting can be flipped back per session via `extra_prefs={"fission.webContentIsolationStrategy": 1}`. - -### Added -- `tests/test_cross_origin_iframe.py`: 4 unit + 5 e2e regression sentinels for cross-origin iframe interaction. The e2e layer runs entirely offline against two local HTTP servers on `127.0.0.1` (two ports = two SOP origins) and covers `page.frames` URL tracking, `content_frame()`, `frame.evaluate()`, `frame_locator(...).locator(...)`, and end-to-end `dispatch_event("click")` for plain, sandboxed and titled iframes. A future FF upgrade or fingerprint A/B that flips the pref back to `1` will fail the suite before shipping. - -### Unchanged -- `BINARY_VERSION` stays at `firefox-7`. Python-only release; no new Firefox build was needed. - -## [0.1.7] - 2026-05-21 - -### Fixed -- [#18](https://github.com/feder-cr/invisible_playwright/issues/18): Tab crash when running with `headless=True` on Windows on pages that trigger cross-process navigation. Two separate bugs that only manifested together: (1) the Chromium content sandbox at default level 6 puts content processes on `kAlternateWinstation`, but the wrapper hides the browser window on its own alt-desktop (`CreateDesktop` for headless on Windows). Mismatched desktops → cross-process navigations couldn't reparent windows → content process exits cleanly and Playwright fires `page.on('crash')`. (2) The canvas2d `getImageData` stealth spoof wrote to a read-only mapped `DataSourceSurface`. On GPU-backed canvases that memory is write-protected → segfault during the final `getImageData` at page unload. Wrapper now sets `security.sandbox.content.level=4` in the alt-desktop workaround set, and `firefox-7` ships the source fix that moves the noise to the JS array's writable backing buffer. - -### Changed -- `BINARY_VERSION` bumped from `firefox-5` to `firefox-7`. `firefox-6` was rolled back when its partial fix turned out to be wrong (the iframe-burst hypothesis was a dead end; bisection in the evening found the real two-bug cause documented above). - -## [0.1.6] - 2026-05-21 - -### Added -- `profile_dir=` kwarg on `InvisiblePlaywright` (sync + async). When set, the session uses `firefox.launch_persistent_context()` so cookies, localStorage, sessionStorage, extensions, cache and prefs are kept on disk between runs. `__enter__` returns a `BrowserContext` directly: `with InvisiblePlaywright(profile_dir=p) as ctx: ctx.new_page()`. Pair with a stable `seed=` to also pin the fingerprint identity across runs. First run creates the dir; subsequent runs reuse it. - -### Fixed -- `launch_persistent_context(timezone_id="…")` no longer times out at 180s. Root cause: `juggler/content/main.js` calls `docShell.overrideTimezone(...)` on every navigation; the patched Firefox up to firefox-4 didn't expose that IDL method on `nsIDocShell`, so the call threw `TypeError: docShell.overrideTimezone is not a function`. On the non-persistent path the error fired *after* launch and was harmless; on the persistent path it blocked the launch handshake. `firefox-5` ships the C++ method (see `patch.md` section 19); this release removes the firefox-4 era Python workaround that was filtering `locale`/`timezone_id` out of the persistent context kwargs. - -### Changed -- `BINARY_VERSION` bumped from `firefox-4` to `firefox-5`. The Python source delta is JS/Python only; the new Firefox build adds 50 lines of C++ in `docshell/base/nsIDocShell.idl` + `nsDocShell.cpp`. - ## [0.1.5] - 2026-05-20 ### Fixed @@ -73,7 +22,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ## [0.1.3] - 2026-05-19 ### Changed -- `BINARY_VERSION` bumped from `firefox-2` to `firefox-3`. The new archives on both Windows and Linux are built from a clean clone of [feder-cr/invisible_firefox#stealth/150](https://github.com/feder-cr/invisible_firefox/tree/stealth/150) — the consolidated source-of-truth fork (renamed from `feder-cr/firefox`; the companion `feder-cr/firefox-stealth` patches repo was deleted, all patches now live as commits on top of `mozilla-firefox/firefox`). +- `BINARY_VERSION` bumped from `firefox-2` to `firefox-3`. The new archives on both Windows and Linux are built from a clean clone of [feder-cr/invisible-firefox#stealth/150](https://github.com/feder-cr/invisible-firefox/tree/stealth/150) — the consolidated source-of-truth fork (renamed from `feder-cr/firefox`; the companion `feder-cr/firefox-stealth` patches repo was deleted, all patches now live as commits on top of `mozilla-firefox/firefox`). - The patched Firefox archive now ships the **proper C++ implementation** of `windowUtils.jugglerSendMouseEvent`, replacing the JS shim from 0.1.2. ### C++ fixes landed in this release @@ -84,7 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - **C7 (partial)**: storage stub for `nsIDocShell.languageOverride`. Workaround `InvisiblePlaywright(locale="")` recommended until full BC FIELD port lands. ### Verified -- Both archives built from same source: feder-cr/invisible_firefox commit `68906f1f9c55`. +- Both archives built from same source: feder-cr/invisible-firefox commit `68906f1f9c55`. - Windows + Linux smoke suite green: launch, `ctx.new_page()`, `page.mouse.{move,down,up,click,wheel}`, `navigator.webdriver=false`, sannysoft 32/33 PASS. - SHA256 published in `checksums.txt` on the `firefox-3` release. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8eb110d..b56e5d3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,7 +7,7 @@ Thanks for your interest in improving this project. Contributions are welcome vi - **Bug?** Open a [bug report](https://github.com/feder-cr/invisible_playwright/issues/new?template=bug_report.yml). - **Idea?** Open a [feature request](https://github.com/feder-cr/invisible_playwright/issues/new?template=feature_request.yml). - **Security issue?** Do **not** open a public issue — see [SECURITY.md](SECURITY.md). -- **The C++ patches** live in the companion repo [feder-cr/invisible_firefox](https://github.com/feder-cr/invisible_firefox) (branch `stealth/150`). Bugs in fingerprint spoofing usually belong there. +- **The C++ patches** live in the companion repo [feder-cr/invisible-firefox](https://github.com/feder-cr/invisible-firefox) (branch `stealth/150`). Bugs in fingerprint spoofing usually belong there. ## Scope @@ -18,7 +18,7 @@ This repository ships the **Python wrapper** (`invisible_playwright`) around a p - Binary download/caching, CLI, proxy plumbing - Tests, docs, examples, packaging -Out of scope (belongs in `invisible_firefox`): +Out of scope (belongs in `invisible-firefox`): - Changes to the Firefox C++ source - New preferences exposed by the patched binary @@ -65,7 +65,7 @@ Before opening, please: - Search [existing issues](https://github.com/feder-cr/invisible_playwright/issues) — the bug may already be tracked. - Reproduce on the **latest release** if possible. -- Confirm the issue is in the Python wrapper, not the patched Firefox itself. If a fingerprint is leaking or a detector flags the browser, open the issue at `feder-cr/invisible_firefox` instead. +- Confirm the issue is in the Python wrapper, not the patched Firefox itself. If a fingerprint is leaking or a detector flags the browser, open the issue at `feder-cr/invisible-firefox` instead. Include: diff --git a/README.md b/README.md index 0a75d61..205617c 100644 --- a/README.md +++ b/README.md @@ -6,26 +6,56 @@ [![Firefox 150.0.1](https://img.shields.io/badge/firefox-150.0.1-orange.svg)](https://www.mozilla.org/firefox/) [![GitHub release](https://img.shields.io/github/v/release/feder-cr/invisible_playwright.svg)](https://github.com/feder-cr/invisible_playwright/releases) [![GitHub stars](https://img.shields.io/github/stars/feder-cr/invisible_playwright.svg?style=social)](https://github.com/feder-cr/invisible_playwright/stargazers) -[![browser launches](https://img.shields.io/github/downloads/feder-cr/invisible_firefox/usage-counter/total?label=browser%20launches&color=blue)](https://github.com/feder-cr/invisible_firefox/releases/tag/usage-counter) [![LinkedIn](https://img.shields.io/badge/LinkedIn-Federico%20Elia-0A66C2?logo=linkedin&logoColor=white)](https://it.linkedin.com/in/federico-elia-5199951b6) -**Stealth Firefox that passes every bot detection test. Drop-in Playwright replacement, fingerprint patched at the C++ level, not a JavaScript shim.** +A patched Firefox **100% Playwright-compatible** that passes the hardest browser-fingerprint detectors in the wild. -![invisible_playwright - 5/5 detection suites passed](docs/screenshots/hero.gif) +## Results + +### Google reCAPTCHA v3 - **0.90 / 1.0** + +Top-tier score. Google classifies the session as "very likely a human". Most anti-detect stacks plateau around 0.3-0.7. + +![reCAPTCHA score 0.90](docs/screenshots/recaptcha_score.png) + +### Fingerprint Pro - **bot: not detected, VPN: false, tampering: false, dev tools: not detected** + +FingerprintJS Pro's full Smart Signals battery flips every flag to "Not detected". Browser correctly identified as Firefox 150 on Windows 10. Confidence score 0.9. + +![FingerprintPro not detected](docs/screenshots/fingerprintpro.png) + +### CreepJS - **0 lies**, fingerprint is internally coherent + +No contradictions between headless hints, spoofed values, and real rendering output. That "0 lies" is what kills most anti-detect browsers: one inconsistency (e.g. Chrome UA + Firefox WebGL) and the trust score collapses. + +![CreepJS 0 lies](docs/screenshots/creepjs.png) + +### BrowserLeaks WebRTC - **no public IP leak** + +WebRTC srflx address is the proxy egress IP; host candidates are private LAN. The real public IP never leaks via STUN, even on pages that configure their own ICE servers. Stock Firefox exposes an mDNS hostname (e.g. `abc-1234.local`) as a host ICE candidate, which is itself a stable per-session signal detectors fingerprint. invisible_playwright replaces host candidates with synthetic private-LAN IPs that match the spoofed network, removing the mDNS tell. + +![WebRTC no leaks](docs/screenshots/webrtc.png) + +### bot.sannysoft.com - **all checks pass** + +Every row green: WebDriver not present, Chrome-only properties absent, plugin/mime/languages arrays coherent, permissions API correct, iframe/source window checks pass. + +![Sannysoft all green](docs/screenshots/sannysoft.png) + +--- ## Why it's powerful - -**Most other anti-detect browsers patch Chromium at the JavaScript level** - they override `navigator`, `WebGLRenderingContext.getParameter`, canvas APIs, and so on via injected scripts. This has two fatal problems: +**Most anti-detect browsers patch Chromium at the JavaScript level** - they override `navigator`, `WebGLRenderingContext.getParameter`, canvas APIs, and so on via injected scripts. This has two fatal problems: 1. **JS patches are detectable.** Anti-bots enumerate native function `.toString()`, check descriptor configurability, compare property enumeration order, watch for prototype mutations. Every patch leaves a fingerprint of its own. CreepJS has an entire battery of "lies detectors" built around this. 2. **Chromium itself is now suspect.** Residential-proxy bot traffic is overwhelmingly Chromium-based, so detectors weight anything Chromium-shaped as risky by default. Chromium-based forks inherit Chrome's open-source layers (BoringSSL, Blink, V8, ANGLE) cleanly, but they still cannot fully match Chrome in practice: Chrome ships closed-source components on top (Widevine, proprietary codecs, Google Update / Safe Browsing endpoints) that flip detectable JS feature flags and network signals, and forks lag Chrome's release cadence by days to weeks, leaving telltale version-specific behaviours that detectors lock onto. **invisible_playwright patches Firefox at the C++ level.** The spoofed values come back out through the normal Gecko paths - there is no JS shim, no override, no `Object.defineProperty`. **From the page's point of view, the browser is just telling the truth.** Anti-bot lie-detectors have nothing to latch onto. -invisible_playwright spoofs **all the layers that matter, together, coherently**: Navigator, screen, GPU/WebGL, Canvas, fonts, audio, WebRTC, timezone, DevTools detection, SOCKS5 auth, and the rest. See [feder-cr/invisible_firefox](https://github.com/feder-cr/invisible_firefox) for the full per-layer breakdown of which C++ files are patched and why. +invisible_playwright spoofs **all the layers that matter, together, coherently** — Navigator, screen, GPU/WebGL, Canvas, fonts, audio, WebRTC, timezone, DevTools detection, SOCKS5 auth, and the rest. See [feder-cr/invisible-firefox](https://github.com/feder-cr/invisible-firefox) for the full per-layer breakdown of which C++ files are patched and why. Everything is driven by preferences - no hardcoded values in the binary. You change one pref, you change the spoofed value. @@ -33,21 +63,23 @@ Everything is driven by preferences - no hardcoded values in the binary. You cha ## How it compares -**CloakBrowser** ships a similar pitch for Chromium, but its binary is **closed source** (the source-level patches are not published, you only get the compiled output), and it still hits the Chromium reCAPTCHA ceiling. The commercial anti-detect browsers (**Multilogin**, **GoLogin**, AdsPower, Dolphin, Kameleo) are paid SaaS that overlay JS-layer spoofing on a patched Chromium. Managed profiles are nice but raw detection bypass sits below both Camoufox and us. +Commercial anti-detect browsers (Multilogin Mimic, GoLogin Orbita, AdsPower, Dolphin Anty) ship patched Chromium and apply most spoofing at the JavaScript layer. A few (Kameleo, Multilogin Stealthfox) also offer Firefox-based profiles, but the spoofing pattern is the same: runtime overrides on top of an unmodified rendering engine. That's the ceiling - and it's a low one. -| | invisible_playwright | Camoufox | CloakBrowser | Multilogin | +| | invisible_playwright | Multilogin / GoLogin | AdsPower / Dolphin | Kameleo | |---|---|---|---|---| -| Engine | Firefox 150 | Firefox (~1 year old base) | Chromium | Chromium fork | -| Patch depth | C++ source | C++ source | C++ source | JS overrides | -| Maintenance | Active | Gap (~1 year) | Active | Active SaaS | -| Open source | ✅ MIT | ✅ MPL | ❌ Closed source | ❌ Closed source | -| `.toString()` clean | ✅ | ✅ | ✅ | ❌ Detectable shims | -| Canvas / WebGL / Audio | ✅ C++ | ⚠️ Drift vs current FF | ✅ C++ | ⚠️ JS override | -| SOCKS5 auth | ✅ Patched | ❌ | ⚠️ Playwright proxy | ⚠️ Varies | -| **reCAPTCHA v3 score** | **0.90** | ~0.3-0.5 | ~0.3-0.5 | ~0.3-0.6 | -| FP Pro - bot detected | ✅ Not detected | ❌ Detected | ❌ Detected | ❌ Detected | -| CreepJS lies | ✅ 0 | ❌ Multiple | ✅ 0 | ❌ Multiple | -| Cost | Free | Free | Free | From $99/mo | +| Engine | Firefox (open source) | Chromium fork | Chromium fork | Chromium | +| Patch depth | C++ source | JS overrides | JS overrides | JS overrides | +| `.toString()` clean | ✅ Native Gecko path | ❌ Detectable shims | ❌ Detectable shims | ❌ Detectable shims | +| Canvas / WebGL | ✅ C++ level | ⚠️ JS override | ⚠️ JS override | ⚠️ JS override | +| SOCKS5 auth | ✅ Patched | ⚠️ Varies | ⚠️ Varies | ❌ | +| Self-hosted | ✅ | ❌ SaaS | ❌ SaaS | ❌ Cloud | +| reCAPTCHA v3 score | **0.90** | ~0.3-0.6 | ~0.3-0.5 | ~0.3-0.5 | +| FP Pro - bot detected | ✅ Not detected | ❌ Detected | ❌ Detected | ❌ Detected | +| FP Pro - tampering | ✅ Not detected | ❌ Detected | ❌ Detected | ❌ Detected | +| FP Pro - VPN flag | ✅ false | ❌ true | ❌ true | ❌ true | +| CreepJS lies | ✅ 0 | ❌ multiple | ❌ multiple | ❌ multiple | + +Competitor scores reflect our own testing on Windows 10 against the same five detection suites used above; results may vary with their evolving builds. --- @@ -58,7 +90,7 @@ pip install git+https://github.com/feder-cr/invisible_playwright.git python -m invisible_playwright fetch # one-time ~100 MB download, SHA256-verified ``` -Supported platforms: **Windows x86_64**, **Linux x86_64 / arm64**, **macOS arm64 / x86_64**. On macOS the app is ad-hoc signed (not notarized): if Gatekeeper complains, clear the quarantine flag once with `xattr -dr com.apple.quarantine` on the cached `Firefox.app`. +Supported platforms: **Windows x86_64**, **Linux x86_64**. --- @@ -140,21 +172,6 @@ with InvisiblePlaywright(proxy=proxy) as browser: Schemes supported: `socks5`, `socks4`, `http`, `https`. Auth works on all of them (SOCKS5 via patched `nsProtocolProxyService.cpp`, HTTP/HTTPS via Playwright). DNS is routed through the proxy by default, no local leak. -### Timezone - -The browser timezone follows `timezone=`: - -```python -# default: timezone is auto-derived from the egress IP (proxy egress if a -# proxy is set, otherwise the host's own public IP) -with InvisiblePlaywright(proxy=proxy) as browser: - ... - -# explicit IANA zone always wins — the only way to force a specific zone -with InvisiblePlaywright(proxy=proxy, timezone="America/New_York") as browser: - ... -``` - ### Pinning specific fingerprint fields By default everything comes from `seed`. To force specific values while the rest stays seed-derived: @@ -181,7 +198,6 @@ Full list of pinnable keys, how pinning interacts with the Bayesian sampler, and ```bash invisible_playwright fetch # download the binary if missing -invisible_playwright fetch --force # re-download even if cached invisible_playwright path # print the absolute path to the cached binary invisible_playwright version # wrapper and binary versions invisible_playwright clear-cache # remove all cached binaries @@ -199,4 +215,4 @@ invisible_playwright takes a different angle than the major Firefox-hardening pr ## License -MIT - see [LICENSE](LICENSE). The patched Firefox binary is distributed under the MPL-2.0 (Firefox upstream license). The C++ patches against mozilla-central that produce that binary are at [feder-cr/invisible_firefox](https://github.com/feder-cr/invisible_firefox). +MIT - see [LICENSE](LICENSE). The patched Firefox binary is distributed under the MPL-2.0 (Firefox upstream license). The C++ patches against mozilla-central that produce that binary are at [feder-cr/invisible-firefox](https://github.com/feder-cr/invisible-firefox). diff --git a/SECURITY.md b/SECURITY.md index 83959a2..19dbc11 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -41,7 +41,7 @@ In scope: Out of scope here (report to the relevant project): -- Vulnerabilities in the patched Firefox C++ source — open a private report at [feder-cr/invisible_firefox](https://github.com/feder-cr/invisible_firefox/security/advisories/new) +- Vulnerabilities in the patched Firefox C++ source — open a private report at [feder-cr/invisible-firefox](https://github.com/feder-cr/invisible-firefox/security/advisories/new) - Vulnerabilities in upstream Firefox / mozilla-central — report to Mozilla per https://www.mozilla.org/security/ - Vulnerabilities in third-party dependencies (`playwright`, `requests`, etc.) — report to those projects directly diff --git a/docs/screenshots/hero.gif b/docs/screenshots/hero.gif deleted file mode 100644 index eadbf1b..0000000 Binary files a/docs/screenshots/hero.gif and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index 4bf9262..02f4cfc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "invisible-playwright" -version = "0.2.0" +version = "0.1.5" description = "Playwright wrapper for a patched Firefox with deterministic stealth profile." readme = "README.md" requires-python = ">=3.11" @@ -22,15 +22,13 @@ classifiers = [ dependencies = [ "playwright>=1.40", "platformdirs>=4", - "requests[socks]>=2.31", - "maxminddb>=2.2", - "tzdata>=2024.1", + "requests>=2.31", "tqdm>=4.66", "pywin32>=306; sys_platform == 'win32'", ] [project.optional-dependencies] -dev = ["pytest>=7", "pytest-mock>=3", "responses>=0.24", "build>=1", "pytest-rerunfailures>=14", "playwright>=1.40"] +dev = ["pytest>=7", "pytest-mock>=3", "responses>=0.24", "build>=1"] [tool.pytest.ini_options] markers = [ @@ -41,12 +39,6 @@ markers = [ "linux_only: tests that require Linux platform", ] addopts = "-m 'not slow and not e2e'" -# tests/playwright-upstream/ is a vendored Microsoft Playwright test suite -# used for compatibility verification on demand. It has its own deps -# (pixelmatch with API not matching our version) and a conftest that fails -# collection in our env. Run it explicitly with --override-ini for compat -# audits, not on every push. -norecursedirs = ["playwright-upstream"] [project.scripts] invisible-playwright = "invisible_playwright.cli:main" diff --git a/scripts/ci_drive_gate.py b/scripts/ci_drive_gate.py deleted file mode 100644 index 2b6ebf0..0000000 --- a/scripts/ci_drive_gate.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python3 -"""CI drive gate — the firefox-N catcher. - -A raw `firefox --screenshot` proves nothing about automation: a juggler-less -binary renders a screenshot just fine and ships broken (firefox-8 did exactly -that). This DRIVES the binary the way users will — Playwright launches it over -the juggler pipe and exercises real paths. - -Two levels (see `--full`): - - SMOKE (default — run on ALL 5 legs, on every binary's native runner): - launch over juggler-pipe → navigate a real http://127.0.0.1 page → assert a - response, the Firefox UA, navigator.webdriver falsy, and a DOM read. This is - the firefox-8 catcher (a juggler-less binary throws TargetClosedError on - launch) plus a base stealth + drivability check. It is intentionally LIGHT: - the free hosted runners — windows-latest especially — are content-process - unstable under a heavy headless interaction sequence (clicks/moves cascade - into "context destroyed" / selector-timeout / eval-CSP), so the gate that - must be GREEN on every leg stays minimal and reliable. - - FULL (`--full` — run on the historically-reliable Linux leg): - SMOKE plus mouse + keyboard input (firefox-2 / issue #9: - jugglerSendMouseEvent/synthesizeMouseEvent), canvas determinism (stealth - seed must be per-session), and navigator-surface tells. The interaction code - is platform-identical JS (it lives in omni.ja), so exercising it on one - reliable leg catches a regression for ALL platforms; win interaction is - additionally covered by local pre-release testing. - -NOT covered here: WebGL determinism (needs SWGL, false-fails headless) and the -faithful cross-origin iframe test (issue #20) — both live in the local realness -gate. All checks here are headless, no screenshot (GPU-free), loopback-only -(no external network / proxy / secrets) → safe in public CI. - -Robustness: a real loopback HTTP page (NOT data: / about:blank — those get -re-normalized / carry an eval-blocking CSP), arrow-function evaluates (never -eval'd), and up to 2 retries on transient context-destroyed/detached/timeout. -A genuinely broken binary fails ALL attempts → the gate fails. - -Usage: python ci_drive_gate.py [--full] -Exit 0 + "DRIVE GATE OK ..." on success; non-zero with a reason on failure. -""" -from __future__ import annotations - -import http.server -import socketserver -import sys -import threading - -HTML = ( - "dt" - "

hello-drive

" - "" - "" - "" - "" -).encode() - -CANVAS_DRAW = ( - "() => {const c=document.createElement('canvas');c.width=c.height=16;" - "const g=c.getContext('2d');g.fillStyle='#08f';g.fillRect(0,0,16,16);" - "g.fillStyle='#f40';g.fillText('s',2,12);return c.toDataURL();}" -) - -_TRANSIENT = ("context was destroyed", "frame was detached", "target closed", - "because of a navigation", "timeout", "blocked by csp") - - -class _Handler(http.server.BaseHTTPRequestHandler): - def do_GET(self): # noqa: N802 - self.send_response(200) - self.send_header("Content-Type", "text/html; charset=utf-8") - self.send_header("Content-Length", str(len(HTML))) - self.end_headers() - self.wfile.write(HTML) - - def log_message(self, *a): # silence per-request stderr noise - pass - - -def _start_server(): - srv = socketserver.TCPServer(("127.0.0.1", 0), _Handler) - threading.Thread(target=srv.serve_forever, daemon=True).start() - return srv, srv.server_address[1] - - -def _drive(exe: str, url: str, full: bool) -> str: - """One full drive attempt. Returns the UA on success; raises on failure.""" - from playwright.sync_api import sync_playwright - - with sync_playwright() as p: - browser = p.firefox.launch(executable_path=exe, headless=True) - try: - page = browser.new_page() - resp = page.goto(url, wait_until="load") - assert resp and resp.ok, f"navigation to {url} failed: {resp.status if resp else 'no response'}" - ua = page.evaluate("() => navigator.userAgent") - webdriver = page.evaluate("() => navigator.webdriver") - text = page.evaluate("() => document.getElementById('x').textContent") - - inter = {} - if full: - # firefox-2 / issue-#9 catcher: real mouse + keyboard over juggler. - page.wait_for_selector("#b") - page.mouse.move(20, 20) - page.mouse.move(120, 90) # synthesizeMouseEvent path - page.click("#b") # mousedown/up/click → listener fires - page.click("#inp") - page.keyboard.type("ok") - inter["clicked"] = page.evaluate("() => window.__clicked") - inter["moves"] = page.evaluate("() => window.__moves") - inter["typed"] = page.evaluate("() => document.getElementById('inp').value") - inter["canvas_a"] = page.evaluate(CANVAS_DRAW) - inter["canvas_b"] = page.evaluate(CANVAS_DRAW) - inter["langs"] = page.evaluate("() => navigator.languages.length") - inter["plugins"] = page.evaluate("() => navigator.plugins instanceof PluginArray") - finally: - browser.close() - - # SMOKE asserts (always). - assert "Firefox" in ua, f"unexpected UA (binary not driving correctly): {ua!r}" - assert text == "hello-drive", f"DOM/JS roundtrip failed: {text!r}" - assert not webdriver, f"navigator.webdriver leaked True (stealth regression): {webdriver!r}" - - if full: - assert inter["clicked"] == 1, "page.click() did not fire the click listener — mouse-event synthesis broken (firefox-2 class)" - assert inter["moves"] >= 1, "page.mouse.move() produced no mousemove — jugglerSendMouseEvent regression" - assert inter["typed"] == "ok", f"page.keyboard.type() failed: {inter['typed']!r}" - assert inter["canvas_a"] == inter["canvas_b"], "canvas non-deterministic across identical draws (stealth seed broken → bot tell)" - assert inter["langs"] and inter["langs"] > 0, "navigator.languages empty (headless tell)" - assert inter["plugins"], "navigator.plugins is not a PluginArray (headless tell)" - return ua - - -def main(exe: str, full: bool) -> int: - srv, port = _start_server() - url = f"http://127.0.0.1:{port}/" - level = "full" if full else "smoke" - extras = "http+click+mousemove+keyboard+canvas-determinism+navsurface" if full else "http+ua+webdriver+dom" - last = None - try: - for attempt in (1, 2, 3): - try: - ua = _drive(exe, url, full) - if attempt > 1: - print(f"(note: drive succeeded on attempt {attempt} after a transient error)") - print(f"DRIVE GATE OK [{level}] | UA={ua} | {extras}=ok") - return 0 - except Exception as e: # noqa: BLE001 — gate: any failure must surface - last = e - msg = str(e).lower() - if attempt < 3 and any(t in msg for t in _TRANSIENT): - print(f"(transient error on attempt {attempt}, retrying): {e}", file=sys.stderr) - continue - break - finally: - srv.shutdown() - print(f"DRIVE GATE FAILED [{level}]: {last}", file=sys.stderr) - return 1 - - -if __name__ == "__main__": - args = sys.argv[1:] - full = "--full" in args - positional = [a for a in args if not a.startswith("--")] - if len(positional) != 1: - print("usage: ci_drive_gate.py [--full]", file=sys.stderr) - sys.exit(2) - sys.exit(main(positional[0], full)) diff --git a/scripts/gen_release_notes.py b/scripts/gen_release_notes.py deleted file mode 100644 index 7ea4296..0000000 --- a/scripts/gen_release_notes.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -"""Generate the GitHub release body for a firefox-N build from the actual -invisible_firefox commits that went into it. - -The release tag (firefox-N) lives on the wrapper, but the binary's changes live -on the SOURCE repo (feder-cr/invisible_firefox). We never deep-clone that history -(it's a full Firefox fork); instead we use GitHub's compare API to list the -commits between the PREVIOUS release's source commit and this one, and turn their -subject lines into a short human-readable "What changed" list. - - - The previous release's source commit comes from its ``source-commit.txt`` - asset (this script's own output uploads one for the next run to read). - - If there's no previous source commit (first automated release) or the compare - fails, we fall back to a body WITHOUT the changelog section — publishing must - never break on note generation. - -This is NOT an LLM and NOT a raw ``git log`` dump: it filters out the -non-user-facing commits (docs/chore/ci/test/style) and prints the remaining -subjects as plain bullets. Quality rides on writing good commit subjects. - -Usage: - python scripts/gen_release_notes.py --tag firefox-10 --current \ - [--prev-sha ] [--source-repo feder-cr/invisible_firefox] - # reads GITHUB_TOKEN from the env for the compare API (optional for public). -""" -from __future__ import annotations - -import argparse -import json -import os -import re -import sys -import urllib.request -import urllib.error - -# Conventional-commit prefixes that never belong in user-facing release notes. -_SKIP = re.compile(r"^(docs|chore|ci|test|style|build)(\(|:)", re.I) - - -def _api(url: str, token: str | None) -> dict: - headers = {"Accept": "application/vnd.github+json", - "User-Agent": "invisible-playwright-release-notes"} - if token: - headers["Authorization"] = f"Bearer {token}" - req = urllib.request.Request(url, headers=headers) - with urllib.request.urlopen(req, timeout=30) as r: - return json.load(r) - - -def changelog_bullets(source_repo: str, prev_sha: str, current_sha: str, - token: str | None) -> list[str]: - """Return the user-facing commit subjects in prev_sha..current_sha, or [].""" - if not prev_sha or not current_sha or prev_sha == current_sha: - return [] - url = f"https://api.github.com/repos/{source_repo}/compare/{prev_sha}...{current_sha}" - try: - data = _api(url, token) - except (urllib.error.URLError, urllib.error.HTTPError, ValueError) as e: - print(f"[gen_release_notes] compare API failed ({e}); no changelog section", - file=sys.stderr) - return [] - bullets: list[str] = [] - for c in data.get("commits", []): - subject = (c.get("commit", {}).get("message") or "").splitlines()[0].strip() - if not subject or _SKIP.match(subject): - continue - bullets.append(subject.rstrip(".")) - return bullets - - -def build_body(tag: str, current_sha: str, bullets: list[str]) -> str: - m = re.search(r"(\d+)", tag) - n = int(m.group(1)) if m else None - prev_label = f"firefox-{n - 1}" if n else "the previous build" - short = (current_sha or "")[:8] - - parts = ["Patched Firefox 150.0.1, the stealth build invisible_playwright drives.", ""] - if bullets: - parts.append(f"What changed since {prev_label}:") - parts += [f"- {b}" for b in bullets] - parts.append("") - parts += [ - "Builds: Linux x86_64, Linux arm64, Windows x86_64, macOS arm64, macOS x86_64.", - "", - "Most people won't grab these by hand. The wrapper fetches the right one for " - "your platform on first run:", - "", - " pip install git+https://github.com/feder-cr/invisible_playwright", - "", - "If you do download manually, `checksums.txt` has the SHA256s. The macOS builds " - "are ad-hoc signed (not notarized), so clear the quarantine flag: " - "`xattr -dr com.apple.quarantine Firefox.app`", - ] - if short: - parts += ["", f"Built from invisible_firefox @{short}."] - return "\n".join(parts) - - -def main() -> int: - ap = argparse.ArgumentParser() - ap.add_argument("--tag", required=True, help="release tag, e.g. firefox-10") - ap.add_argument("--current", required=True, help="invisible_firefox SHA this build was built from") - ap.add_argument("--prev-sha", default="", help="previous release's source SHA (omit for none)") - ap.add_argument("--source-repo", default="feder-cr/invisible_firefox") - args = ap.parse_args() - - token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN") - bullets = changelog_bullets(args.source_repo, args.prev_sha, args.current, token) - sys.stdout.write(build_body(args.tag, args.current, bullets)) - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/playwright_pin.txt b/scripts/playwright_pin.txt deleted file mode 100644 index 094d6ad..0000000 --- a/scripts/playwright_pin.txt +++ /dev/null @@ -1 +0,0 @@ -1.55.0 diff --git a/scripts/run_e2e.py b/scripts/run_e2e.py deleted file mode 100644 index bec1c7d..0000000 --- a/scripts/run_e2e.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env python3 -"""Run the FULL e2e suite (every test that opens the browser) against a binary. - -The 127 ``@pytest.mark.e2e`` tests are excluded from the default `pytest` run -(`addopts = -m 'not slow and not e2e'`) because they need a real Firefox binary -and a display, and they skip themselves when no binary is available. That makes -them easy to forget — and "we can't afford for something to not work". This is -the gate that runs them all, deliberately, against a chosen binary. - -It is the MANDATORY pre-release e2e gate: run it green against the freshly-built -release binary BEFORE un-drafting a firefox-N (alongside the fppro + WebRTC -realness gates). It is NOT in the public CI drive-gate — the hosted runners are -content-process unstable under a heavy headless interaction sequence (see -70-known-bugs / 60-ci-release-pipeline); this runs locally on reliable hardware. - -Flake-resilience: under full-suite load a couple of interaction tests (dblclick, -hover/mouseenter) can flake even though they pass 3/3 in isolation, so failures -are reran up to twice on the known transient signatures. A genuinely broken -binary fails all attempts. The webrtc e2e fake a TCP-only SOCKS locally (no -proxy/secrets), so the whole suite is offline. - -Usage: - python scripts/run_e2e.py - python scripts/run_e2e.py # uses $INVPW_BINARY_PATH -""" -from __future__ import annotations - -import os -import subprocess -import sys -from pathlib import Path - -_RERUN_SIGNATURES = "Timeout|context was destroyed|was detached|not visible|because of a navigation|TargetClosed" - - -def main() -> int: - binary = sys.argv[1] if len(sys.argv) > 1 else os.environ.get("INVPW_BINARY_PATH") - if not binary: - print("usage: run_e2e.py (or set INVPW_BINARY_PATH)", file=sys.stderr) - return 2 - if not Path(binary).exists(): - print(f"ERROR: binary not found: {binary}", file=sys.stderr) - return 2 - - env = dict(os.environ) - # One setting drives the whole suite: conftest's firefox_binary fixture and - # the webrtc e2e both resolve from these. - env["INVPW_BINARY_PATH"] = binary - env["STEALTHFOX_E2E_BINARY"] = binary - - repo = Path(__file__).resolve().parent.parent - cmd = [ - sys.executable, "-m", "pytest", - "-m", "e2e", - "-o", "addopts=", # override the default 'not e2e' deselection - "--reruns", "2", "--reruns-delay", "1", - "--only-rerun", _RERUN_SIGNATURES, - "-p", "no:cacheprovider", - "-q", "--tb=short", - ] + sys.argv[2:] - print(f"[run_e2e] binary={binary}") - print(f"[run_e2e] {' '.join(cmd)}") - return subprocess.run(cmd, cwd=repo, env=env).returncode - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/src/invisible_playwright/__init__.py b/src/invisible_playwright/__init__.py index 0871021..6bae9f3 100644 --- a/src/invisible_playwright/__init__.py +++ b/src/invisible_playwright/__init__.py @@ -15,30 +15,8 @@ Quickstart: page = browser.new_page() page.click("#submit") # expanded into a Bezier trajectory """ -from .config import get_default_args, get_default_stealth_prefs -from .constants import BINARY_VERSION, FIREFOX_UPSTREAM_VERSION -from ._geo import GeoTimezoneError, resolve_session_timezone -from .download import ensure_binary, ensure_geoip_mmdb from .launcher import InvisiblePlaywright +from .constants import BINARY_VERSION, FIREFOX_UPSTREAM_VERSION -from importlib.metadata import PackageNotFoundError, version as _pkg_version - -try: - __version__ = _pkg_version("invisible-playwright") -except PackageNotFoundError: - # Editable / source checkout without an install record: fall back to a - # marker rather than risk shipping a stale hardcoded string. - __version__ = "0.0.0+unknown" - -__all__ = [ - "InvisiblePlaywright", - "ensure_binary", - "ensure_geoip_mmdb", - "get_default_stealth_prefs", - "get_default_args", - "resolve_session_timezone", - "GeoTimezoneError", - "BINARY_VERSION", - "FIREFOX_UPSTREAM_VERSION", - "__version__", -] +__version__ = "0.1.0" +__all__ = ["InvisiblePlaywright", "BINARY_VERSION", "FIREFOX_UPSTREAM_VERSION", "__version__"] diff --git a/src/invisible_playwright/_fpforge/_sampler.py b/src/invisible_playwright/_fpforge/_sampler.py index 692f600..5653db8 100644 --- a/src/invisible_playwright/_fpforge/_sampler.py +++ b/src/invisible_playwright/_fpforge/_sampler.py @@ -84,12 +84,6 @@ _FONT_POOL = _load("font_pool.json") _FONT_CORE: list = _FONT_POOL["core"] _FONT_OPTIONAL: list = _FONT_POOL["optional"] _CPT_FONTS_OPT = _load("cpt_fonts_optional_given_class.json")["table"] -# Browsing-history pool + CPT (per-class probabilities for visited sites). -# Drives _recaptcha_seed's cookie pre-seed: each persona ends up with a -# coherent list of ~15-30 visited sites whose categories correlate with -# gpu_class (workstation → dev-heavy, integrated_old → shop+news-heavy). -_BROWSING_POOL: list = _load("browsing_pool.json")["entries"] -_CPT_BROWSING = _load("cpt_browsing_given_class.json")["table"] # ═══════════════════════════════════════════════════════════════════════ @@ -288,33 +282,6 @@ def derive_font_whitelist(gpu_class: str, rng) -> str: return derive_font_prefs(gpu_class, rng)["whitelist"] -# ═══════════════════════════════════════════════════════════════════════ -# BROWSING HISTORY (Bayesian: per-site P(visited|gpu_class)) -# ═══════════════════════════════════════════════════════════════════════ -def derive_browsing_history(gpu_class: str, rng) -> list: - """Sample which sites this persona has visited recently. - - Each site in the pool has a per-class probability (CPT). We sample - independently per-site, producing a list of dicts: - [{"name": "github.com", "category": "dev", "cookie_profile": "ga_cf"}, ...] - - Sum of CPT probabilities per class is tuned to land ~15-30 visited sites - on average — an established-user signature. Sorted by name for stable - output across runs of the same seed. - """ - cpt = _CPT_BROWSING.get(gpu_class) - if cpt is None: - cpt = _CPT_BROWSING["mid_range"] - visited: list = [] - for entry in _BROWSING_POOL: - name = entry["name"] - p = cpt.get(name, 0.3) # default 0.3 for missing CPT row - if rng.random() < p: - visited.append(dict(entry)) # copy to avoid mutating pool - visited.sort(key=lambda e: e["name"]) - return visited - - # ═══════════════════════════════════════════════════════════════════════ # PUBLIC API: Forge # ═══════════════════════════════════════════════════════════════════════ @@ -383,12 +350,6 @@ class Forge: bundle["gpu_class"], self._rng ).items() }, - # Bayesian browsing history (per-class P(visited|gpu_class)). - # Consumed by _recaptcha_seed.py to seed coherent cookie history - # when invisible_playwright is launched with prep_recaptcha=True. - "browsing_history": derive_browsing_history( - bundle["gpu_class"], self._rng - ), } diff --git a/src/invisible_playwright/_fpforge/data/browsing_pool.json b/src/invisible_playwright/_fpforge/data/browsing_pool.json deleted file mode 100644 index 6e98cd9..0000000 --- a/src/invisible_playwright/_fpforge/data/browsing_pool.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "_comment": [ - "Pool of everyday websites used by the browsing_history node.", - "Each entry: { name, category, cookie_profile }.", - "- name: bare domain (no scheme, no leading dot).", - "- category: dev / shop / news / reference / media / community / misc.", - "- cookie_profile: short tag pointing to a cookie-template recipe used by", - " _recaptcha_seed.py to generate concrete cookies (so heavy-analytics sites", - " get _ga+_gid+OneTrust, simple sites get just _ga, dev tools get GH-style).", - "Add new entries here + add per-class probabilities in cpt_browsing_given_class.json." - ], - "entries": [ - {"name": "youtube.com", "category": "media", "cookie_profile": "ga_only"}, - {"name": "wikipedia.org", "category": "reference", "cookie_profile": "minimal"}, - {"name": "mozilla.org", "category": "reference", "cookie_profile": "ga_consent"}, - {"name": "w3schools.com", "category": "dev", "cookie_profile": "ga_consent_clarity"}, - {"name": "mdn.io", "category": "dev", "cookie_profile": "minimal"}, - {"name": "duckduckgo.com", "category": "reference", "cookie_profile": "minimal"}, - {"name": "github.com", "category": "dev", "cookie_profile": "ga_cf"}, - {"name": "stackoverflow.com", "category": "dev", "cookie_profile": "ga_consent_clarity"}, - {"name": "npmjs.com", "category": "dev", "cookie_profile": "ga_consent"}, - {"name": "gitlab.com", "category": "dev", "cookie_profile": "ga_cf"}, - {"name": "pypi.org", "category": "dev", "cookie_profile": "minimal"}, - {"name": "docs.python.org", "category": "dev", "cookie_profile": "minimal"}, - {"name": "rust-lang.org", "category": "dev", "cookie_profile": "ga_consent"}, - {"name": "go.dev", "category": "dev", "cookie_profile": "ga_consent"}, - {"name": "amazon.com", "category": "shop", "cookie_profile": "ga_consent_clarity"}, - {"name": "ebay.com", "category": "shop", "cookie_profile": "ga_consent"}, - {"name": "etsy.com", "category": "shop", "cookie_profile": "ga_consent_clarity"}, - {"name": "bestbuy.com", "category": "shop", "cookie_profile": "ga_consent_clarity"}, - {"name": "target.com", "category": "shop", "cookie_profile": "ga_consent_clarity"}, - {"name": "nytimes.com", "category": "news", "cookie_profile": "ga_consent_clarity"}, - {"name": "cnn.com", "category": "news", "cookie_profile": "ga_consent"}, - {"name": "bbc.com", "category": "news", "cookie_profile": "ga_consent"}, - {"name": "theguardian.com", "category": "news", "cookie_profile": "ga_consent_clarity"}, - {"name": "reuters.com", "category": "news", "cookie_profile": "ga_consent"}, - {"name": "apnews.com", "category": "news", "cookie_profile": "ga_consent"}, - {"name": "washingtonpost.com", "category": "news", "cookie_profile": "ga_consent"}, - {"name": "techcrunch.com", "category": "news", "cookie_profile": "ga_consent_clarity"}, - {"name": "theverge.com", "category": "news", "cookie_profile": "ga_consent"}, - {"name": "arstechnica.com", "category": "news", "cookie_profile": "ga_consent"}, - {"name": "wired.com", "category": "news", "cookie_profile": "ga_consent_clarity"}, - {"name": "engadget.com", "category": "news", "cookie_profile": "ga_consent"}, - {"name": "9to5mac.com", "category": "news", "cookie_profile": "ga_consent"}, - {"name": "medium.com", "category": "community", "cookie_profile": "ga_consent"}, - {"name": "dev.to", "category": "community", "cookie_profile": "ga_consent"}, - {"name": "reddit.com", "category": "community", "cookie_profile": "ga_cf"}, - {"name": "news.ycombinator.com", "category": "community", "cookie_profile": "minimal"}, - {"name": "quora.com", "category": "community", "cookie_profile": "ga_consent_clarity"}, - {"name": "stackexchange.com", "category": "community", "cookie_profile": "ga_consent_clarity"}, - {"name": "imdb.com", "category": "media", "cookie_profile": "ga_consent_clarity"}, - {"name": "rottentomatoes.com", "category": "media", "cookie_profile": "ga_consent"}, - {"name": "metacritic.com", "category": "media", "cookie_profile": "ga_consent"}, - {"name": "allrecipes.com", "category": "misc", "cookie_profile": "ga_consent_clarity"}, - {"name": "epicurious.com", "category": "misc", "cookie_profile": "ga_consent"}, - {"name": "tripadvisor.com", "category": "misc", "cookie_profile": "ga_consent_clarity"}, - {"name": "weather.com", "category": "reference", "cookie_profile": "ga_consent"}, - {"name": "timeanddate.com", "category": "reference", "cookie_profile": "ga_consent"}, - {"name": "thesaurus.com", "category": "reference", "cookie_profile": "ga_consent_clarity"}, - {"name": "kayak.com", "category": "shop", "cookie_profile": "ga_consent_clarity"}, - {"name": "booking.com", "category": "shop", "cookie_profile": "ga_consent_clarity"}, - {"name": "airbnb.com", "category": "shop", "cookie_profile": "ga_consent"} - ] -} diff --git a/src/invisible_playwright/_fpforge/data/cpt_browsing_given_class.json b/src/invisible_playwright/_fpforge/data/cpt_browsing_given_class.json deleted file mode 100644 index b2e3b1a..0000000 --- a/src/invisible_playwright/_fpforge/data/cpt_browsing_given_class.json +++ /dev/null @@ -1,138 +0,0 @@ -{ - "_comment": [ - "Per-class probability that a persona of a given gpu_class has visited each", - "site in the pool. Used by the browsing_history node to derive a coherent", - "visited-domain list per persona.", - "", - "Probabilities are tuned so each class samples ~15-30 sites on average", - "(sum across all 50 entries falls in that range), giving an established-user", - "look. Categories are biased by class:", - " - workstation/high_end: higher P(dev) + high P(news/media)", - " - mid_range: balanced", - " - low_end/integrated_*: lower P(dev), higher P(shop/news/reference)", - "", - "Missing class falls back to mid_range via Node CPT pool fallback." - ], - "table": { - "workstation": { - "youtube.com": 0.80, "wikipedia.org": 0.85, "mozilla.org": 0.70, - "w3schools.com": 0.40, "mdn.io": 0.55, "duckduckgo.com": 0.45, - "github.com": 0.95, "stackoverflow.com": 0.90, "npmjs.com": 0.65, - "gitlab.com": 0.50, "pypi.org": 0.55, "docs.python.org": 0.60, - "rust-lang.org": 0.35, "go.dev": 0.30, - "amazon.com": 0.70, "ebay.com": 0.25, "etsy.com": 0.15, - "bestbuy.com": 0.45, "target.com": 0.30, - "nytimes.com": 0.55, "cnn.com": 0.40, "bbc.com": 0.55, - "theguardian.com": 0.45, "reuters.com": 0.40, "apnews.com": 0.30, - "washingtonpost.com": 0.40, - "techcrunch.com": 0.65, "theverge.com": 0.60, "arstechnica.com": 0.65, - "wired.com": 0.50, "engadget.com": 0.35, "9to5mac.com": 0.30, - "medium.com": 0.55, "dev.to": 0.40, "reddit.com": 0.70, - "news.ycombinator.com": 0.65, "quora.com": 0.20, "stackexchange.com": 0.60, - "imdb.com": 0.45, "rottentomatoes.com": 0.25, "metacritic.com": 0.20, - "allrecipes.com": 0.20, "epicurious.com": 0.15, "tripadvisor.com": 0.30, - "weather.com": 0.55, "timeanddate.com": 0.30, "thesaurus.com": 0.25, - "kayak.com": 0.30, "booking.com": 0.35, "airbnb.com": 0.30 - }, - "high_end": { - "youtube.com": 0.85, "wikipedia.org": 0.80, "mozilla.org": 0.60, - "w3schools.com": 0.45, "mdn.io": 0.45, "duckduckgo.com": 0.40, - "github.com": 0.85, "stackoverflow.com": 0.80, "npmjs.com": 0.50, - "gitlab.com": 0.40, "pypi.org": 0.45, "docs.python.org": 0.50, - "rust-lang.org": 0.30, "go.dev": 0.25, - "amazon.com": 0.75, "ebay.com": 0.30, "etsy.com": 0.20, - "bestbuy.com": 0.50, "target.com": 0.35, - "nytimes.com": 0.50, "cnn.com": 0.50, "bbc.com": 0.50, - "theguardian.com": 0.40, "reuters.com": 0.35, "apnews.com": 0.30, - "washingtonpost.com": 0.35, - "techcrunch.com": 0.60, "theverge.com": 0.65, "arstechnica.com": 0.60, - "wired.com": 0.50, "engadget.com": 0.40, "9to5mac.com": 0.35, - "medium.com": 0.50, "dev.to": 0.35, "reddit.com": 0.75, - "news.ycombinator.com": 0.55, "quora.com": 0.25, "stackexchange.com": 0.55, - "imdb.com": 0.55, "rottentomatoes.com": 0.35, "metacritic.com": 0.30, - "allrecipes.com": 0.25, "epicurious.com": 0.20, "tripadvisor.com": 0.30, - "weather.com": 0.55, "timeanddate.com": 0.30, "thesaurus.com": 0.25, - "kayak.com": 0.30, "booking.com": 0.40, "airbnb.com": 0.30 - }, - "mid_range": { - "youtube.com": 0.85, "wikipedia.org": 0.75, "mozilla.org": 0.45, - "w3schools.com": 0.40, "mdn.io": 0.30, "duckduckgo.com": 0.35, - "github.com": 0.55, "stackoverflow.com": 0.55, "npmjs.com": 0.30, - "gitlab.com": 0.25, "pypi.org": 0.25, "docs.python.org": 0.30, - "rust-lang.org": 0.15, "go.dev": 0.15, - "amazon.com": 0.80, "ebay.com": 0.40, "etsy.com": 0.30, - "bestbuy.com": 0.55, "target.com": 0.40, - "nytimes.com": 0.45, "cnn.com": 0.55, "bbc.com": 0.45, - "theguardian.com": 0.35, "reuters.com": 0.30, "apnews.com": 0.30, - "washingtonpost.com": 0.30, - "techcrunch.com": 0.45, "theverge.com": 0.50, "arstechnica.com": 0.40, - "wired.com": 0.45, "engadget.com": 0.35, "9to5mac.com": 0.30, - "medium.com": 0.45, "dev.to": 0.25, "reddit.com": 0.70, - "news.ycombinator.com": 0.30, "quora.com": 0.35, "stackexchange.com": 0.40, - "imdb.com": 0.60, "rottentomatoes.com": 0.40, "metacritic.com": 0.35, - "allrecipes.com": 0.35, "epicurious.com": 0.25, "tripadvisor.com": 0.40, - "weather.com": 0.60, "timeanddate.com": 0.25, "thesaurus.com": 0.30, - "kayak.com": 0.35, "booking.com": 0.45, "airbnb.com": 0.40 - }, - "low_end": { - "youtube.com": 0.85, "wikipedia.org": 0.70, "mozilla.org": 0.35, - "w3schools.com": 0.30, "mdn.io": 0.20, "duckduckgo.com": 0.30, - "github.com": 0.30, "stackoverflow.com": 0.30, "npmjs.com": 0.15, - "gitlab.com": 0.10, "pypi.org": 0.10, "docs.python.org": 0.15, - "rust-lang.org": 0.05, "go.dev": 0.05, - "amazon.com": 0.85, "ebay.com": 0.50, "etsy.com": 0.40, - "bestbuy.com": 0.55, "target.com": 0.45, - "nytimes.com": 0.40, "cnn.com": 0.60, "bbc.com": 0.40, - "theguardian.com": 0.30, "reuters.com": 0.25, "apnews.com": 0.30, - "washingtonpost.com": 0.25, - "techcrunch.com": 0.30, "theverge.com": 0.35, "arstechnica.com": 0.25, - "wired.com": 0.40, "engadget.com": 0.30, "9to5mac.com": 0.25, - "medium.com": 0.35, "dev.to": 0.15, "reddit.com": 0.65, - "news.ycombinator.com": 0.15, "quora.com": 0.45, "stackexchange.com": 0.25, - "imdb.com": 0.65, "rottentomatoes.com": 0.45, "metacritic.com": 0.35, - "allrecipes.com": 0.45, "epicurious.com": 0.30, "tripadvisor.com": 0.45, - "weather.com": 0.65, "timeanddate.com": 0.25, "thesaurus.com": 0.35, - "kayak.com": 0.35, "booking.com": 0.50, "airbnb.com": 0.40 - }, - "integrated_modern": { - "youtube.com": 0.85, "wikipedia.org": 0.70, "mozilla.org": 0.40, - "w3schools.com": 0.35, "mdn.io": 0.25, "duckduckgo.com": 0.35, - "github.com": 0.40, "stackoverflow.com": 0.40, "npmjs.com": 0.20, - "gitlab.com": 0.15, "pypi.org": 0.20, "docs.python.org": 0.20, - "rust-lang.org": 0.10, "go.dev": 0.10, - "amazon.com": 0.80, "ebay.com": 0.40, "etsy.com": 0.30, - "bestbuy.com": 0.50, "target.com": 0.40, - "nytimes.com": 0.40, "cnn.com": 0.55, "bbc.com": 0.45, - "theguardian.com": 0.35, "reuters.com": 0.30, "apnews.com": 0.30, - "washingtonpost.com": 0.30, - "techcrunch.com": 0.40, "theverge.com": 0.45, "arstechnica.com": 0.30, - "wired.com": 0.40, "engadget.com": 0.30, "9to5mac.com": 0.25, - "medium.com": 0.40, "dev.to": 0.20, "reddit.com": 0.65, - "news.ycombinator.com": 0.25, "quora.com": 0.40, "stackexchange.com": 0.35, - "imdb.com": 0.60, "rottentomatoes.com": 0.40, "metacritic.com": 0.30, - "allrecipes.com": 0.40, "epicurious.com": 0.25, "tripadvisor.com": 0.40, - "weather.com": 0.60, "timeanddate.com": 0.25, "thesaurus.com": 0.30, - "kayak.com": 0.35, "booking.com": 0.45, "airbnb.com": 0.40 - }, - "integrated_old": { - "youtube.com": 0.75, "wikipedia.org": 0.65, "mozilla.org": 0.30, - "w3schools.com": 0.20, "mdn.io": 0.10, "duckduckgo.com": 0.25, - "github.com": 0.15, "stackoverflow.com": 0.20, "npmjs.com": 0.05, - "gitlab.com": 0.05, "pypi.org": 0.05, "docs.python.org": 0.10, - "rust-lang.org": 0.02, "go.dev": 0.02, - "amazon.com": 0.85, "ebay.com": 0.55, "etsy.com": 0.45, - "bestbuy.com": 0.55, "target.com": 0.50, - "nytimes.com": 0.45, "cnn.com": 0.65, "bbc.com": 0.40, - "theguardian.com": 0.30, "reuters.com": 0.25, "apnews.com": 0.35, - "washingtonpost.com": 0.30, - "techcrunch.com": 0.20, "theverge.com": 0.25, "arstechnica.com": 0.15, - "wired.com": 0.30, "engadget.com": 0.20, "9to5mac.com": 0.20, - "medium.com": 0.30, "dev.to": 0.05, "reddit.com": 0.55, - "news.ycombinator.com": 0.05, "quora.com": 0.55, "stackexchange.com": 0.15, - "imdb.com": 0.70, "rottentomatoes.com": 0.50, "metacritic.com": 0.35, - "allrecipes.com": 0.55, "epicurious.com": 0.35, "tripadvisor.com": 0.50, - "weather.com": 0.70, "timeanddate.com": 0.30, "thesaurus.com": 0.40, - "kayak.com": 0.40, "booking.com": 0.55, "airbnb.com": 0.40 - } - } -} diff --git a/src/invisible_playwright/_fpforge/profile.py b/src/invisible_playwright/_fpforge/profile.py index fcdf024..16c52a4 100644 --- a/src/invisible_playwright/_fpforge/profile.py +++ b/src/invisible_playwright/_fpforge/profile.py @@ -120,11 +120,6 @@ class Profile: webgl: WebGLProfile fonts: List[str] dark_theme: bool - # Bayesian browsing-history: list of {name, category, cookie_profile} - # dicts sampled from data/browsing_pool.json with per-class CPT. Used - # by _recaptcha_seed.py to build a coherent cookie pre-seed when the - # caller opts in via Stealthfox(prep_recaptcha=True). - browsing_history: List[Dict[str, str]] = field(default_factory=list) _raw: Dict[str, Any] = field(default_factory=dict, repr=False, compare=False) def to_prefs_dict(self) -> Dict[str, Any]: @@ -260,6 +255,5 @@ def generate_profile(seed: int, pin: Optional[Dict[str, Any]] = None) -> Profile webgl=WebGLProfile(msaa_samples=int(raw["msaa_samples"])), fonts=fonts, dark_theme=bool(raw["dark_theme"]), - browsing_history=list(raw.get("browsing_history") or []), _raw=raw, ) diff --git a/src/invisible_playwright/_geo.py b/src/invisible_playwright/_geo.py deleted file mode 100644 index 7423b2b..0000000 --- a/src/invisible_playwright/_geo.py +++ /dev/null @@ -1,218 +0,0 @@ -"""Resolve the session timezone from the egress IP (``timezone="auto"``). - -Approach B: discover the egress IP with one HTTP request — routed *through the -proxy* when one is set, otherwise a direct request that sees the host's own -public IP — then map IP → IANA timezone with an offline mmdb -(``daijro/geoip-all-in-one``, downloaded + cached by ``download.py``). - -Precedence (see ``resolve_session_timezone``): - - explicit IANA → unchanged explicit always wins - "" / "auto" → egress ALWAYS resolve. With a proxy, from the proxy - egress IP; without a proxy, from the host's - own public IP. This is the default. - -On failure: - with a proxy → raise a foreign proxy paired with the host TZ is - the precise ``timezone_mismatch`` signal, so - we fail loudly rather than fall back silently. - without a proxy → "" (host) the host TZ is a safe default, so a transient - lookup failure must not break the launch. -""" -from __future__ import annotations - -import ipaddress -from typing import Any, Dict, NamedTuple, Optional -from urllib.parse import quote - -import requests - - -class GeoTimezoneError(RuntimeError): - """Raised when ``timezone="auto"`` cannot resolve a valid IANA zone.""" - - -# Plain-text IP echo endpoints (each returns just the caller's public IP). -_IP_ECHO_ENDPOINTS = ( - "https://api.ipify.org", - "https://icanhazip.com", - "https://checkip.amazonaws.com", -) - -_SOCKS_SCHEMES = ("socks5://", "socks4://", "socks://") - - -def _proxy_is_set(proxy: Optional[Dict[str, str]]) -> bool: - if not proxy: - return False - server = (proxy.get("server") or "").strip() - return bool(server) and server.lower() != "direct://" - - -def _proxies_for_requests(proxy: Dict[str, str]) -> Dict[str, str]: - """Translate our proxy dict into a ``requests`` proxies mapping. - - SOCKS5 uses the ``socks5h`` scheme so DNS is resolved proxy-side (matches - ``network.proxy.socks_remote_dns=True`` in the Firefox path). HTTP/HTTPS - pass through unchanged. Credentials are URL-encoded. - """ - server = (proxy.get("server") or "").strip() - low = server.lower() - if low.startswith("socks5://") or low.startswith("socks://"): - scheme = "socks5h" - elif low.startswith("socks4://"): - scheme = "socks4" - elif low.startswith("https://"): - scheme = "https" - else: - scheme = "http" - - host_port = server.split("://", 1)[1] if "://" in server else server - user = proxy.get("username") or "" - pwd = proxy.get("password") or "" - if user: - auth = f"{quote(user, safe='')}:{quote(pwd, safe='')}@" - else: - auth = "" - url = f"{scheme}://{auth}{host_port}" - return {"http": url, "https": url} - - -def discover_egress_ip( - proxy: Optional[Dict[str, str]] = None, *, timeout: float = 10.0 -) -> str: - """Return the public egress IP. - - Routes the request through ``proxy`` when given (SOCKS support requires - ``requests[socks]`` / PySocks); with ``proxy=None`` it makes a direct - request that sees the host's own public IP. Tries each echo endpoint in - turn; raises :class:`GeoTimezoneError` if none return a valid IP. - """ - proxies = _proxies_for_requests(proxy) if proxy else None - last_err: Optional[Exception] = None - for url in _IP_ECHO_ENDPOINTS: - try: - resp = requests.get(url, proxies=proxies, timeout=timeout) - resp.raise_for_status() - ip = resp.text.strip() - ipaddress.ip_address(ip) # validate (raises ValueError if not an IP) - return ip - except Exception as exc: # noqa: BLE001 - try the next endpoint - last_err = exc - continue - raise GeoTimezoneError( - f"could not discover the proxy egress IP via {len(_IP_ECHO_ENDPOINTS)} " - f"endpoints (last error: {last_err!r}). For SOCKS proxies make sure " - f"requests[socks] / PySocks is installed." - ) - - -def ip_to_timezone(ip: str, mmdb_path: Any) -> str: - """Map ``ip`` to its IANA timezone using the offline mmdb. - - Reads the standard MaxMind ``location.time_zone`` field and validates it - against the system tz database. Raises :class:`GeoTimezoneError` if the IP - is absent from the DB or the zone is missing / not a valid IANA name. - """ - import maxminddb - - with maxminddb.open_database(str(mmdb_path)) as reader: - record = reader.get(ip) - if not record: - raise GeoTimezoneError(f"egress IP {ip} not present in the geoip database") - tz = ((record.get("location") or {}) if isinstance(record, dict) else {}).get( - "time_zone" - ) - if not tz: - raise GeoTimezoneError(f"no timezone for egress IP {ip} in the geoip database") - from zoneinfo import ZoneInfo, ZoneInfoNotFoundError - - try: - ZoneInfo(tz) - except (ZoneInfoNotFoundError, ValueError) as exc: - raise GeoTimezoneError( - f"geoip returned an invalid IANA zone {tz!r} for {ip}: {exc}" - ) from exc - return tz - - -class SessionGeo(NamedTuple): - """Geo facts resolved once per session from a single egress round-trip. - - ``timezone`` follows the precedence in the module docstring. - ``egress_ip`` is the proxy egress IP (the IP the *outside world* sees) when - a proxy is set, else ``None`` — it feeds the WebRTC srflx override, which is - only meaningful behind a proxy (a direct connection's real STUN already - reports the truthful public IP, so we leave it alone). - """ - - timezone: str - egress_ip: Optional[str] - - -def prepare_session_geo( - timezone: str, proxy: Optional[Dict[str, str]] -) -> SessionGeo: - """Resolve the session timezone AND the proxy egress IP in ONE round-trip. - - The egress IP is discovered once and reused for both the timezone mapping - (when ``timezone`` is ``""``/``"auto"``) and the WebRTC public-IP override. - Timezone precedence is identical to :func:`resolve_session_timezone`; the - egress IP is best-effort for the WebRTC side (a discovery failure that the - timezone path doesn't need won't break the launch — but if the timezone - path *does* need it behind a proxy, that path still fails loudly). - """ - from .download import ensure_geoip_mmdb - - tz = (timezone or "").strip() - proxy_set = _proxy_is_set(proxy) - - # One discovery, reused below. Behind a proxy we always want the egress IP - # (for WebRTC) regardless of the timezone setting. - egress_ip: Optional[str] = None - egress_err: Optional[Exception] = None - if proxy_set: - try: - egress_ip = discover_egress_ip(proxy) - except Exception as exc: # noqa: BLE001 - egress_err = exc - - # Timezone resolution — same precedence as resolve_session_timezone. - if tz and tz.lower() != "auto": - return SessionGeo(tz, egress_ip) # explicit IANA wins - try: - ip = egress_ip if proxy_set else discover_egress_ip(None) - if ip is None: # proxy set but discovery failed above - raise egress_err or GeoTimezoneError("egress IP discovery failed") - return SessionGeo(ip_to_timezone(ip, ensure_geoip_mmdb()), egress_ip) - except Exception: - if proxy_set: - raise # fail-early behind a proxy (timezone_mismatch trap) - return SessionGeo("", None) # no proxy: host TZ is a safe fallback - - -def resolve_session_timezone( - timezone: str, proxy: Optional[Dict[str, str]] -) -> str: - """Map the user's ``timezone`` setting to a concrete IANA zone (or ``""``). - - Timezone-only path (no WebRTC side effects): an explicit IANA zone wins and - triggers NO network call; ``""``/``"auto"`` resolve from the egress IP. The - launch path uses :func:`prepare_session_geo` instead (which additionally - returns the egress IP for WebRTC); this standalone resolver is kept for - third-party integrations that only want the zone. See the module docstring - for the precedence table. - """ - tz = (timezone or "").strip() - if tz and tz.lower() != "auto": - return tz # explicit IANA wins — no egress lookup - from .download import ensure_geoip_mmdb - - proxy_set = _proxy_is_set(proxy) - try: - ip = discover_egress_ip(proxy if proxy_set else None) - return ip_to_timezone(ip, ensure_geoip_mmdb()) - except Exception: - if proxy_set: - raise # fail-early behind a proxy (timezone_mismatch trap) - return "" # no proxy: host TZ is a safe fallback diff --git a/src/invisible_playwright/_headless.py b/src/invisible_playwright/_headless.py index 3cfee09..2e4b249 100644 --- a/src/invisible_playwright/_headless.py +++ b/src/invisible_playwright/_headless.py @@ -2,23 +2,18 @@ Playwright's ``headless=True`` flips Firefox onto a different code path — no widget tree, software-only rendering, distinct timing — and anti-bot -systems can spot the divergence. Running the browser *headed* but hidden -gives us the real rendering pipeline while keeping the windows off screen. +systems can spot the divergence. Running the browser *headed* on a +virtual display gives us the real rendering pipeline while keeping the +windows off the user's screen. -Two mechanisms, by platform: - -- **Windows & macOS**: the patched binary cloaks its OWN chrome windows - when ``zoom.stealth.cloak_windows`` is set — ``DWMWA_CLOAK`` (Windows) - / ``NSWindow`` alpha-0 + pinned occlusion-ignore (macOS). The window - renders on the real GPU but never appears on screen, in the taskbar or - the Dock. The launcher injects the pref; nothing host-side is spawned. - -- **Linux**: spawns its own ``Xvfb`` instance and points ``DISPLAY`` at - it (X11/Wayland have no per-window cloak that keeps the GPU rendering). +Linux: spawns its own ``Xvfb`` instance, points ``DISPLAY`` at it. +Windows: creates a hidden desktop via ``CreateDesktop`` and binds the +calling thread to it, so Playwright's child processes inherit it. """ from __future__ import annotations import os +import secrets import subprocess import sys import time @@ -136,40 +131,95 @@ class _LinuxVirtualDisplay: self._display = None -# Windows & macOS: the patched Firefox cloaks its own chrome windows when this -# pref is set (DWMWA_CLOAK / NSWindow alpha-0 + pinned occlusion-ignore), so the -# window renders on the real GPU but never shows on screen / in the taskbar or -# Dock. window_occlusion_tracking is disabled so a hidden window keeps painting. -CLOAK_PREFS = { - "zoom.stealth.cloak_windows": True, - "widget.windows.window_occlusion_tracking.enabled": False, -} +class _WindowsVirtualDesktop: + """A hidden Windows desktop the calling thread is bound to. + Playwright's child processes (node driver → firefox.exe) inherit the + desktop because their ``STARTUPINFO.lpDesktop`` is NULL — Windows uses + the calling thread's desktop in that case. -def cloak_prefs() -> dict: - """Prefs that make the patched binary self-cloak its chrome windows. - - Used on Windows & macOS, where hiding is done inside the binary rather than - with a host-side virtual display. + pywin32 ships ``CreateDesktop`` in ``win32service`` but doesn't expose + ``SetThreadDesktop`` / ``GetThreadDesktop`` as module functions. We + call them directly via ctypes against ``user32.dll``. """ - return dict(CLOAK_PREFS) + + def __init__(self) -> None: + self._desktop = None # PyHDESK from win32service.CreateDesktop + self._original_handle = 0 # raw HDESK int of the previous desktop + + def start(self) -> None: + try: + import win32con # type: ignore + import win32service # type: ignore + except ImportError as e: + raise RuntimeError( + "invisible_playwright headless=True on Windows requires pywin32. " + "Install it: pip install pywin32" + ) from e + + import ctypes + from ctypes import wintypes + user32 = ctypes.windll.user32 + kernel32 = ctypes.windll.kernel32 + + # Save the current desktop handle so we can restore it on stop(). + get_thread_desktop = user32.GetThreadDesktop + get_thread_desktop.argtypes = [wintypes.DWORD] + get_thread_desktop.restype = wintypes.HANDLE + self._original_handle = get_thread_desktop(kernel32.GetCurrentThreadId()) + + name = f"sf_{secrets.token_hex(4)}" + self._desktop = win32service.CreateDesktop( + name, 0, win32con.GENERIC_ALL, None + ) + + # Bind the calling thread to the new desktop. Children spawned + # afterwards (Playwright driver → firefox.exe) inherit it because + # their STARTUPINFO.lpDesktop is NULL. + set_thread_desktop = user32.SetThreadDesktop + set_thread_desktop.argtypes = [wintypes.HANDLE] + set_thread_desktop.restype = wintypes.BOOL + if not set_thread_desktop(int(self._desktop)): + err = ctypes.get_last_error() + raise RuntimeError( + f"SetThreadDesktop failed (GetLastError={err}). " + "The thread cannot have any windows or hooks; close them first." + ) + + def stop(self) -> None: + import ctypes + from ctypes import wintypes + user32 = ctypes.windll.user32 + + if self._original_handle: + try: + set_thread_desktop = user32.SetThreadDesktop + set_thread_desktop.argtypes = [wintypes.HANDLE] + set_thread_desktop.restype = wintypes.BOOL + set_thread_desktop(self._original_handle) + except Exception: + pass + self._original_handle = 0 + + if self._desktop is not None: + try: + self._desktop.CloseDesktop() + except Exception: + pass + self._desktop = None def make_virtual_display(): - """Return a start()/stop()-able virtual display, or ``None`` when the - platform hides windows via the in-binary cloak pref instead. + """Return a started/stoppable virtual-display object for this platform. - - Linux: a fresh ``Xvfb`` (the launcher start()s/stop()s it). - - Windows / macOS: ``None`` — the binary self-cloaks via ``cloak_prefs()``, - injected by the launcher; nothing host-side needs spawning. + InvisiblePlaywright supports Windows x86_64 and Linux x86_64 only. """ + if sys.platform == "win32": + return _WindowsVirtualDesktop() if sys.platform.startswith("linux"): return _LinuxVirtualDisplay() - if sys.platform in ("win32", "darwin"): - return None raise RuntimeError( - f"invisible_playwright supports Windows, macOS and Linux " - f"(got {sys.platform!r})" + f"invisible_playwright supports Windows and Linux only (got {sys.platform!r})" ) diff --git a/src/invisible_playwright/_recaptcha_seed.py b/src/invisible_playwright/_recaptcha_seed.py deleted file mode 100644 index cd998a2..0000000 --- a/src/invisible_playwright/_recaptcha_seed.py +++ /dev/null @@ -1,340 +0,0 @@ -"""Deterministic reCAPTCHA cookie pre-seed. - -Consumes the Bayesian-sampled `browsing_history` from the persona Profile -(see `_fpforge/_sampler.py:derive_browsing_history`). For each visited -site, builds 1-5 realistic cookies whose composition is chosen by the -site's `cookie_profile` tag (analytics-only / consent / cloudflare-bot- -management / etc.). All values seeded deterministically from the persona -seed, so a given persona always presents the SAME cookies across sessions. - -In addition, always seeds 5 cookies on .google.com (NID, CONSENT, SOCS, -_GRECAPTCHA, ENID). Excludes 1P_JAR which was deprecated by Google in 2022 -— including it now is an anachronism flag. - -Public API: - await seed_recaptcha_cookies_async(context, profile, timezone=None) - seed_recaptcha_cookies_sync(context, profile, timezone=None) - -`profile` is an `_fpforge.Profile`; `timezone` is the IANA tz (e.g. -"Europe/Rome") used to derive the CONSENT cookie's language token, so a -European-tz persona gets CONSENT in their language not en+FX. -""" -from __future__ import annotations - -import datetime -import random -import time -from typing import Any, List, Optional - -# URL-safe base64 alphabet (no padding chars). -_B64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_" -_HEX_ALPHABET = "0123456789abcdef" - - -def _sub_seed(seed: int, tag: str) -> int: - """FNV-1a mix → independent PRNG streams per logical bucket from one seed.""" - h = 0xcbf29ce484222325 ^ (seed & 0xFFFFFFFF) - for c in tag.encode("ascii"): - h ^= c - h = (h * 0x100000001b3) & 0xFFFFFFFFFFFFFFFF - return h or 0xdeadbeef - - -def _b64_rand(rng: random.Random, length: int) -> str: - return "".join(rng.choice(_B64_ALPHABET) for _ in range(length)) - - -def _hex_rand(rng: random.Random, length: int) -> str: - return "".join(rng.choice(_HEX_ALPHABET) for _ in range(length)) - - -def _yyyymmdd_utc(ts: int) -> str: - return datetime.datetime.utcfromtimestamp(ts).strftime("%Y%m%d") - - -# IANA timezone -> (country_code, lang) for CONSENT cookie coherence. -# Real EU users get CONSENT with `++NNN`; non-EU gets `en+FX+NNN`. -# Default fallback `en+FX+NNN` for any tz not in this map. -_TZ_TO_REGION = { - "Europe/Rome": ("IT", "it"), - "Europe/Berlin": ("DE", "de"), - "Europe/Paris": ("FR", "fr"), - "Europe/Madrid": ("ES", "es"), - "Europe/London": ("GB", "en"), - "Europe/Amsterdam": ("NL", "nl"), - "Europe/Brussels": ("BE", "fr"), - "Europe/Vienna": ("AT", "de"), - "Europe/Zurich": ("CH", "de"), - "Europe/Dublin": ("IE", "en"), - "Europe/Lisbon": ("PT", "pt"), - "Europe/Stockholm": ("SE", "sv"), - "Europe/Oslo": ("NO", "no"), - "Europe/Copenhagen": ("DK", "da"), - "Europe/Helsinki": ("FI", "fi"), - "Europe/Warsaw": ("PL", "pl"), - "Europe/Prague": ("CZ", "cs"), - "Europe/Athens": ("GR", "el"), - "Asia/Tokyo": ("FX", "ja"), - "Asia/Shanghai": ("FX", "zh"), - "Asia/Hong_Kong": ("FX", "zh"), - "Asia/Seoul": ("FX", "ko"), -} - - -def _consent_region_lang(timezone: Optional[str]) -> tuple: - """Map IANA tz → (region_token, lang_2char) for CONSENT cookie. - Default `("FX", "en")` for US/unknown.""" - if timezone and timezone in _TZ_TO_REGION: - return _TZ_TO_REGION[timezone] - return ("FX", "en") - - -# --------------------------------------------------------------------------- -# .google.com cookie batch (always present, regardless of browsing history) -# --------------------------------------------------------------------------- - -def _google_cookies(rng: random.Random, now: int, - timezone: Optional[str] = None) -> List[dict]: - consent_age = rng.randint(60, 720) * 86400 - region, lang = _consent_region_lang(timezone) - # NID 3-digit prefix range broadened to 100-540 to cover historical NID - # versions (137, 105, 511, 525 etc. observed in real captures). - return [ - {"name": "NID", - "value": f"{rng.randint(100, 540)}={_b64_rand(rng, 178)}", - "domain": ".google.com", "path": "/", - "expires": now + 180 * 86400, - "httpOnly": True, "secure": True, "sameSite": "None"}, - {"name": "CONSENT", - "value": f"YES+cb.{_yyyymmdd_utc(now - consent_age)}-" - f"{rng.randint(10, 19):02d}-p{rng.randint(0, 9)}." - f"{lang}+{region}+{rng.randint(100, 999)}", - "domain": ".google.com", "path": "/", - "expires": now + 395 * 86400, - "secure": True, "sameSite": "Lax"}, - # 1P_JAR removed: Google deprecated it in 2022. Including it now is - # an anachronism flag for fingerprinters that look at cookie freshness. - {"name": "SOCS", - "value": f"CAES{_b64_rand(rng, 56)}", - "domain": ".google.com", "path": "/", - "expires": now + 395 * 86400, - "secure": True, "sameSite": "Lax"}, - {"name": "_GRECAPTCHA", - "value": _b64_rand(rng, 124), - "domain": ".google.com", "path": "/", - "expires": now + 180 * 86400, - "secure": True, "sameSite": "None"}, - {"name": "ENID", - "value": _b64_rand(rng, 252), - "domain": ".google.com", "path": "/", - "expires": now + 395 * 86400, - "httpOnly": True, "secure": True, "sameSite": "Lax"}, - ] - - -# --------------------------------------------------------------------------- -# Per-site cookie generators (recipes keyed by site["cookie_profile"]) -# --------------------------------------------------------------------------- - -def _norm_domain(domain: str) -> str: - return domain if domain.startswith(".") else "." + domain - - -def _ga_cookie(rng: random.Random, now: int, domain: str) -> dict: - first_age = rng.randint(7, 395) * 86400 - return {"name": "_ga", - "value": f"GA1.2.{rng.randint(100000000, 999999999)}.{now - first_age}", - "domain": domain, "path": "/", - "expires": now + 395 * 86400, - "secure": True, "sameSite": "Lax"} - - -def _gid_cookie(rng: random.Random, now: int, domain: str) -> dict: - return {"name": "_gid", - "value": f"GA1.2.{rng.randint(100000000, 999999999)}.{now - rng.randint(60, 86400)}", - "domain": domain, "path": "/", - "expires": now + 86400, - "secure": True, "sameSite": "Lax"} - - -def _cf_bm_cookie(rng: random.Random, now: int, domain: str) -> dict: - return {"name": "__cf_bm", - "value": f"{_b64_rand(rng, 43)}.{rng.randint(1700000000, now)}-1-1-1-1", - "domain": domain, "path": "/", - "expires": now + 1800, - "secure": True, "sameSite": "None"} - - -def _onetrust_cookie(rng: random.Random, now: int, domain: str) -> dict: - age_d = rng.randint(7, 365) - iso = datetime.datetime.utcfromtimestamp(now - age_d * 86400).strftime( - "%Y-%m-%dT%H:%M:%S.000Z" - ) - return {"name": "OptanonAlertBoxClosed", - "value": iso, - "domain": domain, "path": "/", - "expires": now + 395 * 86400, - "secure": True, "sameSite": "Lax"} - - -def _cookieyes_cookie(rng: random.Random, now: int, domain: str) -> dict: - return {"name": "cookieyes-consent", - "value": "consentid:" + _b64_rand(rng, 28) + - ",consent:yes,action:yes,necessary:yes,functional:yes,analytics:yes", - "domain": domain, "path": "/", - "expires": now + 395 * 86400, - "secure": True, "sameSite": "Lax"} - - -def _clarity_cookie(rng: random.Random, now: int, domain: str) -> dict: - return {"name": "_clck", - "value": f"{_hex_rand(rng, 8)}|2|f{rng.randint(10, 99)}|0|" - f"{now - rng.randint(60, 180) * 86400}", - "domain": domain, "path": "/", - "expires": now + 365 * 86400, - "secure": True, "sameSite": "Lax"} - - -def _fbp_cookie(rng: random.Random, now: int, domain: str) -> dict: - """Facebook Pixel _fbp = fb...""" - return {"name": "_fbp", - "value": f"fb.1.{(now - rng.randint(60, 30*86400)) * 1000}." - f"{rng.randint(100000000, 9999999999)}", - "domain": domain, "path": "/", - "expires": now + 90 * 86400, - "secure": True, "sameSite": "Lax"} - - -def _gtm_cookie(rng: random.Random, now: int, domain: str) -> dict: - """_dc_gtm_=1 — Google Tag Manager throttle flag.""" - container = f"UA-{rng.randint(10000000, 99999999)}-{rng.randint(1, 9)}" - return {"name": f"_dc_gtm_{container}", - "value": "1", - "domain": domain, "path": "/", - "expires": now + 60, - "secure": True, "sameSite": "Lax"} - - -def _hssrc_cookie(rng: random.Random, now: int, domain: str) -> dict: - """HubSpot referrer flag — small int.""" - return {"name": "__hssrc", - "value": str(rng.randint(1, 5)), - "domain": domain, "path": "/", - "expires": now + 1800, - "secure": True, "sameSite": "Lax"} - - -def _cookies_for_profile(profile: str, rng: random.Random, - now: int, domain: str) -> List[dict]: - """Map cookie_profile tag (from browsing_pool.json) → concrete cookies. - - Each recipe is a realistic combination observed on real production sites - in that category. Cookie age and sub-recipe variance (e.g., OneTrust vs - CookieYes for consent banner) are deterministic from rng. - """ - domain = _norm_domain(domain) - if profile == "minimal": - return [_ga_cookie(rng, now, domain)] - if profile == "ga_only": - out = [_ga_cookie(rng, now, domain), _gid_cookie(rng, now, domain)] - # 30% chance of GTM helper paired with GA - if rng.random() < 0.3: - out.append(_gtm_cookie(rng, now, domain)) - return out - if profile == "ga_cf": - return [_ga_cookie(rng, now, domain), _cf_bm_cookie(rng, now, domain)] - if profile == "ga_consent": - out = [_ga_cookie(rng, now, domain), _gid_cookie(rng, now, domain)] - out.append(_onetrust_cookie(rng, now, domain) if rng.random() < 0.5 - else _cookieyes_cookie(rng, now, domain)) - if rng.random() < 0.4: - out.append(_gtm_cookie(rng, now, domain)) - return out - if profile == "ga_consent_clarity": - # Heavy-tracking site profile: GA + Clarity + consent + often FB pixel - out = [_ga_cookie(rng, now, domain), _gid_cookie(rng, now, domain), - _clarity_cookie(rng, now, domain)] - out.append(_onetrust_cookie(rng, now, domain) if rng.random() < 0.5 - else _cookieyes_cookie(rng, now, domain)) - if rng.random() < 0.5: - out.append(_fbp_cookie(rng, now, domain)) - if rng.random() < 0.4: - out.append(_gtm_cookie(rng, now, domain)) - if rng.random() < 0.25: - out.append(_hssrc_cookie(rng, now, domain)) - return out - # Unknown profile → safe fallback - return [_ga_cookie(rng, now, domain)] - - -# --------------------------------------------------------------------------- -# Public builder -# --------------------------------------------------------------------------- - -def build_cookies(seed: int, - browsing_history: Optional[List[dict]] = None, - now: Optional[int] = None, - timezone: Optional[str] = None) -> List[dict]: - """Build the full cookie list for a persona. - - Args: - seed: persona integer seed (from `Profile.seed`) - browsing_history: list of {name, category, cookie_profile} dicts as - sampled by `_fpforge.derive_browsing_history`. None → empty list - (only the 5 google cookies are returned). - now: unix-seconds timestamp; defaults to current time. Pin for tests. - timezone: IANA tz used to derive CONSENT cookie's `lang+region` token - (e.g. "Europe/Rome" → "it+IT", "America/New_York" → "en+FX"). - """ - ts = now if now is not None else int(time.time()) - cookies: List[dict] = [] - - # 5 .google.com cookies (always) — CONSENT lang derived from tz - rng_g = random.Random(_sub_seed(int(seed), "google")) - cookies.extend(_google_cookies(rng_g, ts, timezone=timezone)) - - # Per-site cookies (deterministic from seed × domain) - for site in (browsing_history or []): - rng_d = random.Random(_sub_seed(int(seed), f"dom:{site['name']}")) - cookies.extend(_cookies_for_profile( - site.get("cookie_profile", "minimal"), rng_d, ts, site["name"] - )) - return cookies - - -def _extract_seed_and_history(profile: Any) -> tuple: - """Accept a Profile object OR a (seed, history) tuple OR just an int seed.""" - if isinstance(profile, int): - return int(profile), [] - seed = int(getattr(profile, "seed")) - history = list(getattr(profile, "browsing_history", []) or []) - return seed, history - - -async def seed_recaptcha_cookies_async(context: Any, profile: Any, - timezone: Optional[str] = None) -> None: - """Async: inject deterministic persona cookies into the context.""" - seed, history = _extract_seed_and_history(profile) - cookies = build_cookies(seed, history, timezone=timezone) - try: - await context.add_cookies(cookies) - except Exception: - pass - - -def seed_recaptcha_cookies_sync(context: Any, profile: Any, - timezone: Optional[str] = None) -> None: - """Sync: inject deterministic persona cookies into the context.""" - seed, history = _extract_seed_and_history(profile) - cookies = build_cookies(seed, history, timezone=timezone) - try: - context.add_cookies(cookies) - except Exception: - pass - - -__all__ = [ - "build_cookies", - "seed_recaptcha_cookies_async", - "seed_recaptcha_cookies_sync", -] diff --git a/src/invisible_playwright/async_api.py b/src/invisible_playwright/async_api.py index e8e1f2b..2933c1e 100644 --- a/src/invisible_playwright/async_api.py +++ b/src/invisible_playwright/async_api.py @@ -3,14 +3,12 @@ from __future__ import annotations import asyncio import secrets -from pathlib import Path from typing import Any, Dict, Optional, Union -from playwright.async_api import Browser, BrowserContext, Playwright, async_playwright +from playwright.async_api import Browser, Playwright, async_playwright from ._fpforge import Profile, generate_profile -from ._geo import prepare_session_geo -from ._headless import cloak_prefs, make_virtual_display +from ._headless import make_virtual_display from ._proxy import configure_proxy as _configure_proxy_shared from .download import ensure_binary from .launcher import _CHROME_H, _CHROME_W, _TASKBAR_H, _tz_env @@ -51,8 +49,6 @@ class InvisiblePlaywright: timezone: str = "", extra_prefs: Optional[Dict[str, Any]] = None, binary_path: Optional[str] = None, - profile_dir: Optional[Union[str, Path]] = None, - prep_recaptcha: bool = False, ) -> None: # See sync launcher: `zoom.stealth.fpp.hw_seed` is int32_t — clamp. self.seed: int = int(seed) if seed is not None else secrets.randbits(31) @@ -65,28 +61,13 @@ class InvisiblePlaywright: self._timezone = timezone self._extra_prefs = extra_prefs self._binary_path = binary_path - self._profile_dir: Optional[Path] = Path(profile_dir) if profile_dir else None - # reCAPTCHA pre-seed gated server-side; respect persistent profile. - self._prep_recaptcha = bool(prep_recaptcha) and self._profile_dir is None self._profile: Profile = generate_profile(self.seed, pin=self._pin) self._pw: Optional[Playwright] = None self._browser: Optional[Browser] = None - self._persistent_context: Optional[BrowserContext] = None self._virtual_display: Any = None - # Proxy egress IP (WebRTC srflx override); discovered in __aenter__. - self._webrtc_egress_ip: Optional[str] = None - async def __aenter__(self) -> Union[Browser, BrowserContext]: + async def __aenter__(self) -> Browser: import sys as _sys - # Resolve timezone="auto" AND discover the proxy egress IP in one - # round-trip, off the event loop, before anything reads self._timezone - # or builds prefs/env. Fail-early if a proxy is set but the egress - # can't be resolved. - _geo = await asyncio.to_thread( - prepare_session_geo, self._timezone, self._proxy - ) - self._timezone = _geo.timezone - self._webrtc_egress_ip = _geo.egress_ip executable = self._binary_path or ensure_binary() prefs = translate_profile_to_prefs( self._profile, @@ -95,42 +76,15 @@ class InvisiblePlaywright: extra_prefs=self._extra_prefs, virtual_display=bool(self._headless and _sys.platform == "win32"), ) - # Windows & macOS hide the headless window via the binary's own cloak - # (DWMWA_CLOAK / NSWindow alpha) — inject the pref so the patched build - # cloaks its chrome windows. setdefault: an explicit user override wins. - # (Mirrors launcher._build_prefs; the sync path always did this, async - # didn't — so async headless=True never cloaked AND crashed below.) - if self._headless and _sys.platform in ("win32", "darwin"): - for _k, _v in cloak_prefs().items(): - prefs.setdefault(_k, _v) - # stealthfox.* is the namespace the binary's Juggler reads (see launcher.py note). - prefs["stealthfox.humanize"] = bool(self._humanize) + prefs["invisible_playwright.humanize"] = bool(self._humanize) if self._humanize: cap = 1.5 if self._humanize is True else float(self._humanize) - prefs["stealthfox.humanize.maxTime"] = str(cap) + prefs["invisible_playwright.humanize.maxTime"] = str(cap) playwright_proxy = _configure_proxy_shared(self._proxy, prefs) pw_headless = self._resolve_headless() env = self._build_env() try: self._pw = await async_playwright().start() - if self._profile_dir is not None: - # See sync launcher for the persistent-context rationale. - self._profile_dir.mkdir(parents=True, exist_ok=True) - # firefox-5 ships the C++ overrideTimezone IDL method (C7 - # chiusura), so locale + timezone_id now propagate cleanly - # to the persistent context without hanging the launch. - self._persistent_context = await self._pw.firefox.launch_persistent_context( - user_data_dir=str(self._profile_dir), - executable_path=str(executable), - headless=pw_headless, - firefox_user_prefs=prefs, - proxy=playwright_proxy, - args=self._extra_args, - env=env, - **self._default_context_kwargs(), - ) - _patch_new_page_sleep(self._persistent_context) - return self._persistent_context self._browser = await self._pw.firefox.launch( executable_path=str(executable), headless=pw_headless, @@ -148,18 +102,12 @@ class InvisiblePlaywright: def _patch_new_context_defaults(self, browser: Browser) -> None: original = browser.new_context defaults = self._default_context_kwargs() - prep = self._prep_recaptcha - profile = self._profile # pass the whole Profile (seed + browsing_history) - tz = self._timezone # used by _recaptcha_seed for CONSENT lang+region async def patched(**kw): merged = dict(defaults) merged.update(kw) ctx = await original(**merged) _patch_new_page_sleep(ctx) - if prep: - from ._recaptcha_seed import seed_recaptcha_cookies_async - await seed_recaptcha_cookies_async(ctx, profile, timezone=tz) return ctx browser.new_context = patched # type: ignore[assignment] @@ -186,12 +134,6 @@ class InvisiblePlaywright: await self._teardown() async def _teardown(self) -> None: - if self._persistent_context is not None: - try: - await self._persistent_context.close() - except Exception: - pass - self._persistent_context = None if self._browser is not None: try: await self._browser.close() @@ -216,29 +158,20 @@ class InvisiblePlaywright: env = _os.environ.copy() if self._timezone: env["TZ"] = _tz_env(self._timezone) - # WebRTC srflx override: feed nICEr's nr_stealth_bridge the proxy egress - # IP (caller's explicit env var wins, else the IP auto-discovered in - # __aenter__) and drop IPv6 from gathering behind a proxy. - webrtc_ip = ( - _os.environ.get("STEALTHFOX_WEBRTC_PUBLIC_IP") - or self._webrtc_egress_ip - ) - if webrtc_ip: - env["STEALTHFOX_WEBRTC_PUBLIC_IP"] = webrtc_ip - env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] = "1" + # Propagate STEALTHFOX_WEBRTC_PUBLIC_IP if the process set it — read + # by nICEr's nr_stealth_bridge to inject a synthetic srflx candidate + # matching the proxy egress IP. This avoids the StaticPref IPC + # propagation timing issue between parent and socket processes. + if _os.environ.get("STEALTHFOX_WEBRTC_PUBLIC_IP"): + env["STEALTHFOX_WEBRTC_PUBLIC_IP"] = _os.environ["STEALTHFOX_WEBRTC_PUBLIC_IP"] return env def _resolve_headless(self) -> bool: if not self._headless: return False vd = make_virtual_display() - # Linux: Xvfb to start. Windows/macOS: make_virtual_display() returns - # None (the binary self-cloaks via cloak_prefs injected in __aenter__), - # so there is nothing to start — guarding the None was the missing piece - # that made async headless=True crash with AttributeError on Windows. - if vd is not None: - vd.start() - self._virtual_display = vd + vd.start() + self._virtual_display = vd return False diff --git a/src/invisible_playwright/cli.py b/src/invisible_playwright/cli.py index eb12067..bb1c687 100644 --- a/src/invisible_playwright/cli.py +++ b/src/invisible_playwright/cli.py @@ -10,15 +10,7 @@ from .constants import BINARY_VERSION, FIREFOX_UPSTREAM_VERSION from .download import cache_root, ensure_binary -def _cmd_fetch(args: argparse.Namespace) -> int: - # --force: re-download even if already cached (drop the cached version dir, - # then let ensure_binary fetch it fresh). Useful to recover a corrupted cache - # or re-pull after a re-published release. - if getattr(args, "force", False): - from .download import cache_dir_for_version - d = cache_dir_for_version() - if d.exists(): - shutil.rmtree(d, ignore_errors=True) +def _cmd_fetch(_args: argparse.Namespace) -> int: path = ensure_binary() print(path) return 0 @@ -52,17 +44,9 @@ def _cmd_clear_cache(_args: argparse.Namespace) -> int: def build_parser() -> argparse.ArgumentParser: p = argparse.ArgumentParser(prog="invisible-playwright", description="invisible_playwright CLI") - # Top-level `--version` / `-V` flag so `python -m invisible_playwright --version` - # works (Python convention), in addition to the existing `version` subcommand. - p.add_argument( - "-V", "--version", action="version", - version=f"invisible_playwright {__version__} (BINARY_VERSION={BINARY_VERSION}, Firefox {FIREFOX_UPSTREAM_VERSION})", - ) - sub = p.add_subparsers(dest="cmd") + sub = p.add_subparsers(dest="cmd", required=True) - fetch_p = sub.add_parser("fetch", help="download the patched Firefox binary") - fetch_p.add_argument("--force", action="store_true", - help="re-download even if already cached") + sub.add_parser("fetch", help="download the patched Firefox binary") sub.add_parser("path", help="print the absolute path to the cached binary") sub.add_parser("version", help="print wrapper and binary versions") sub.add_parser("clear-cache", help="remove all cached binaries") @@ -70,15 +54,7 @@ def build_parser() -> argparse.ArgumentParser: def main(argv: list[str] | None = None) -> int: - parser = build_parser() - args = parser.parse_args(argv) - if args.cmd is None: - # argparse-conventional: print usage + error message to stderr, exit 2. - # We can't keep `required=True` on the subparsers because that breaks - # the top-level `--version` flag (argparse demands a subcommand even - # when --version is the only token). parser.error() preserves the - # original "no subcommand" exit semantics tests expect. - parser.error("a subcommand is required (try --help, --version, or one of: fetch, path, version, clear-cache)") + args = build_parser().parse_args(argv) dispatch = { "fetch": _cmd_fetch, "path": _cmd_path, diff --git a/src/invisible_playwright/config.py b/src/invisible_playwright/config.py deleted file mode 100644 index 2a7300e..0000000 --- a/src/invisible_playwright/config.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Public helpers for building Firefox launch config without using ``InvisiblePlaywright``. - -Use these when you need to call ``playwright.firefox.launch()`` (or -``firefox.launch_persistent_context()``) directly with our patched binary -and stealth prefs, instead of using the ``InvisiblePlaywright`` context -manager. - -Typical caller is an external integration that owns its own browser -lifecycle (a Crawlee/Skyvern/changedetection-style fetcher, a Playwright -Server wrapper, a multi-language harness) and just wants the building -blocks:: - - from playwright.async_api import async_playwright - from invisible_playwright import ensure_binary, get_default_stealth_prefs - - async with async_playwright() as p: - browser = await p.firefox.launch( - executable_path=str(ensure_binary()), - firefox_user_prefs=get_default_stealth_prefs(seed=42), - ) - -For everyday Python usage the ``InvisiblePlaywright`` context manager is -still the recommended entry point; these helpers expose the same internals -without the lifecycle ownership. - -.. note:: - When calling ``firefox.launch()`` yourself, pass ``headless=False`` and - manage the display hiding (Xvfb on Linux, hidden desktop on Windows) - externally. Passing ``headless=True`` directly to Playwright puts - Firefox in true headless mode, which skips the real rendering pipeline - and breaks canvas / audio / WebGL fingerprint coherence. The - ``InvisiblePlaywright`` context manager does this translation - automatically; the public helpers leave it to the caller. -""" -from __future__ import annotations - -import secrets -from typing import Any, Dict, List, Optional, Union - -from ._fpforge import generate_profile -from .prefs import translate_profile_to_prefs - - -def get_default_stealth_prefs( - seed: Optional[int] = None, - *, - pin: Optional[Dict[str, Any]] = None, - locale: str = "en-US", - timezone: str = "", - extra_prefs: Optional[Dict[str, Any]] = None, - humanize: Union[bool, float] = True, - virtual_display: bool = False, -) -> Dict[str, Any]: - """Build a complete ``firefox_user_prefs`` dict for ``firefox.launch()``. - - Same prefs that ``InvisiblePlaywright(seed=..., locale=..., timezone=..., - extra_prefs=..., humanize=...)`` would inject. Use this when you need to - drive ``playwright.firefox.launch()`` yourself. - - Args: - seed: Integer seed for the Bayesian fingerprint sampler. Same seed - produces the same fingerprint. ``None`` generates a fresh - random int31 (matches ``InvisiblePlaywright`` default). - pin: Optional dict forcing specific fingerprint fields while the - rest stays seed-derived. See ``docs/pinning.md``. - locale: BCP-47 tag (e.g. ``"en-US"``). Drives ``Accept-Language`` - and ``navigator.language``. - timezone: IANA timezone (e.g. ``"America/New_York"``). Empty means - use the host TZ. This pure pref builder does NOT resolve - ``"auto"`` (that needs the proxy + a network lookup at launch - time) — pass a concrete zone here, or use ``InvisiblePlaywright`` - / ``resolve_session_timezone(timezone, proxy)`` for ``"auto"``. - extra_prefs: Optional dict overlaid LAST onto the generated prefs. - humanize: When True (default), every mouse move is expanded into - a Bezier trajectory by the patched Juggler. A float caps the - motion in seconds. False disables the behavior. - virtual_display: When True on Windows, apply GPU-disabling prefs - to prevent GPU process crashes on virtual desktops without - D3D11 backend. - - Returns: - Dict ready to pass as ``firefox_user_prefs=`` to - ``playwright.firefox.launch()`` or ``launch_persistent_context()``. - """ - resolved_seed = int(seed) if seed is not None else secrets.randbits(31) - profile = generate_profile(resolved_seed, pin=pin) - prefs = translate_profile_to_prefs( - profile, - locale=locale, - timezone=timezone, - extra_prefs=extra_prefs, - virtual_display=virtual_display, - ) - # stealthfox.* is the namespace the binary's Juggler reads (see launcher.py note). - prefs["stealthfox.humanize"] = bool(humanize) - if humanize: - max_seconds = float(humanize) if not isinstance(humanize, bool) else 1.5 - prefs["stealthfox.humanize.maxTime"] = str(max_seconds) - return prefs - - -def get_default_args() -> List[str]: - """Return the default Firefox CLI args to pass via ``args=``. - - Currently empty list, since all our stealth configuration is delivered - via ``firefox_user_prefs`` rather than CLI flags. Exposed for parity - with the ``cloakbrowser.config.get_default_stealth_args`` pattern and - to future-proof integrations that already wire ``args=[*existing, - *get_default_args()]``. - """ - return [] diff --git a/src/invisible_playwright/constants.py b/src/invisible_playwright/constants.py index fc122ad..43269eb 100644 --- a/src/invisible_playwright/constants.py +++ b/src/invisible_playwright/constants.py @@ -7,14 +7,7 @@ bugfixes don't force a multi-hour Firefox rebuild. from __future__ import annotations # Bump this when a new patched Firefox build is released on GitHub. -BINARY_VERSION: str = "firefox-10" - -# Releases known to be broken — ensure_binary() refuses them with a clear error -# instead of handing the user an unusable binary. firefox-8 was packaged without -# the juggler automation layer, so Playwright cannot drive it (TargetClosedError); -# fixed in firefox-9 (package-manifest.in now ships chrome/juggler). A cached -# firefox-8 from before the bump would otherwise keep being used silently. -BROKEN_VERSIONS: frozenset[str] = frozenset({"firefox-8"}) +BINARY_VERSION: str = "firefox-4" # Underlying Firefox version (for display only; does not drive downloads). FIREFOX_UPSTREAM_VERSION: str = "150.0.1" @@ -26,15 +19,13 @@ BINARY_BASENAME: str = f"firefox-{FIREFOX_UPSTREAM_VERSION}-stealth" def ARCHIVE_NAME(platform_key: str, machine: str) -> str: """Return the platform-specific archive filename. - platform_key: sys.platform ("win32", "linux", "darwin") - machine: platform.machine() ("AMD64", "x86_64", "arm64", "aarch64", ...) + platform_key: sys.platform ("win32", "linux") + machine: platform.machine() ("AMD64", "x86_64", ...) """ pk = platform_key.lower() m = machine.lower() if m in {"amd64", "x86_64"}: arch = "x86_64" - elif m in {"arm64", "aarch64"}: - arch = "arm64" else: raise NotImplementedError(f"unsupported arch: {machine}") @@ -42,39 +33,16 @@ def ARCHIVE_NAME(platform_key: str, machine: str) -> str: return f"{BINARY_BASENAME}-win-{arch}.zip" if pk == "linux": return f"{BINARY_BASENAME}-linux-{arch}.tar.gz" - if pk == "darwin": - return f"{BINARY_BASENAME}-macos-{arch}.tar.gz" raise NotImplementedError(f"unsupported platform: {platform_key}") # Binary entry point relative path inside the extracted archive root. -# macOS ships the .app bundle (renamed to a stable "Firefox.app" by release.yml); -# the wrapper execs the inner binary directly, which sidesteps Gatekeeper. BINARY_ENTRY_REL = { "win32": "firefox.exe", "linux": "firefox", - "darwin": "Firefox.app/Contents/MacOS/firefox", } # GitHub release URL template. The "TODO" owner is resolved at publication time. RELEASE_URL_TEMPLATE = ( "https://github.com/feder-cr/invisible_playwright/releases/download/{tag}/{asset}" ) - -# ───────────────────────────────────────────────────────────────────────── -# GeoIP database (timezone="auto" → resolve IANA zone from proxy egress IP) -# ───────────────────────────────────────────────────────────────────────── -# daijro/geoip-all-in-one merges IP2Location LITE + GeoLite2 + DB-IP into a -# single mmdb (country ISO + coordinates + IANA timezone via tzfpy), rebuilt -# weekly. GPL-3.0, so we DOWNLOAD it at runtime into the user cache (like the -# Firefox binary) rather than bundling it into this MIT package. The `-all` -# variant covers IPv4+IPv6. download.py tracks the LATEST release and refreshes -# weekly; GEOIP_MMDB_VERSION is only the cold-cache fallback when the GitHub -# API is unreachable on a machine that has never downloaded the DB. -GEOIP_REPO: str = "daijro/geoip-all-in-one" -GEOIP_MMDB_VERSION: str = "2026.06.03" -GEOIP_ASSET: str = "geoip-aio-all.mmdb.zip" -GEOIP_MMDB_NAME: str = "geoip-aio-all.mmdb" -GEOIP_RELEASE_URL_TEMPLATE: str = ( - "https://github.com/daijro/geoip-all-in-one/releases/download/{tag}/{asset}" -) diff --git a/src/invisible_playwright/download.py b/src/invisible_playwright/download.py index acb5d49..58a5e8f 100644 --- a/src/invisible_playwright/download.py +++ b/src/invisible_playwright/download.py @@ -5,12 +5,9 @@ import hashlib import os import platform import re -import shutil -import subprocess import sys import tarfile import tempfile -import time import zipfile from pathlib import Path @@ -21,11 +18,6 @@ from .constants import ( ARCHIVE_NAME, BINARY_ENTRY_REL, BINARY_VERSION, - BROKEN_VERSIONS, - GEOIP_ASSET, - GEOIP_MMDB_NAME, - GEOIP_MMDB_VERSION, - GEOIP_RELEASE_URL_TEMPLATE, RELEASE_URL_TEMPLATE, ) @@ -122,39 +114,8 @@ def _extract(archive: Path, dst: Path) -> None: raise RuntimeError(f"unknown archive format: {archive}") -def _post_extract_darwin(app_root: Path, entry: Path) -> None: - """Make an ad-hoc-signed .app launchable on macOS. - - The .app is downloaded via requests (no Finder quarantine attached), but we - strip com.apple.quarantine defensively and ensure the inner binary is - executable. We exec the inner binary directly (not via LaunchServices), so - Gatekeeper's first-launch prompt does not apply; the ad-hoc signature - (applied in release.yml) is what lets the arm64 Mach-O run at all. - """ - app = app_root - # walk up to the .app bundle dir if entry points inside it - for parent in entry.parents: - if parent.name.endswith(".app"): - app = parent - break - try: - subprocess.run(["xattr", "-dr", "com.apple.quarantine", str(app)], check=False) - except FileNotFoundError: - pass - try: - entry.chmod(0o755) - except OSError: - pass - - def ensure_binary(version: str = BINARY_VERSION) -> Path: """Return a path to a runnable Firefox executable. Download if needed.""" - if version in BROKEN_VERSIONS: - raise RuntimeError( - f"{version} is a known-broken release (the juggler automation layer is " - f"missing, so Playwright cannot drive it). Upgrade invisible_playwright " - f"(current BINARY_VERSION={BINARY_VERSION}) or pass a newer version." - ) plat = sys.platform mach = platform.machine() asset = ARCHIVE_NAME(plat, mach) @@ -187,142 +148,6 @@ def ensure_binary(version: str = BINARY_VERSION) -> Path: ) _extract(archive_path, version_dir) - if plat == "darwin": - _post_extract_darwin(version_dir, entry) - if not entry.exists(): raise RuntimeError(f"binary not found after extraction: {entry}") return entry - - -# ───────────────────────────────────────────────────────────────────────── -# GeoIP mmdb (timezone="auto" → map egress IP → IANA zone) -# -# daijro/geoip-all-in-one is rebuilt WEEKLY, so we don't pin a tag. We cache -# the latest mmdb and, once it's older than GEOIP_REFRESH_DAYS, re-check the -# latest release and pull a newer build if one exists. Net effect: no download -# (not even an API call) on a launch within the window; auto-refresh after it; -# a stale cache is reused when offline rather than breaking the launch. -# ───────────────────────────────────────────────────────────────────────── -GEOIP_REFRESH_DAYS = 7 # matches daijro's weekly rebuild cadence - - -def _geoip_root() -> Path: - return cache_root() / "geoip" - - -def _geoip_check_marker() -> Path: - return _geoip_root() / ".last_check" - - -def _cached_geoip_mmdb() -> Path | None: - """Newest cached mmdb across tag dirs, or None. Tag dirs are date strings - (e.g. ``2026.06.03``) so a lexical sort is chronological.""" - root = _geoip_root() - if not root.exists(): - return None - cands = sorted(root.glob("*/*.mmdb")) - return cands[-1] if cands else None - - -def _geoip_cache_fresh(max_age_days: int) -> bool: - marker = _geoip_check_marker() - if not marker.exists(): - return False - return (time.time() - marker.stat().st_mtime) < max_age_days * 86400 - - -def _touch_geoip_marker() -> None: - m = _geoip_check_marker() - m.parent.mkdir(parents=True, exist_ok=True) - m.touch() - - -def _latest_geoip_tag() -> str: - """Latest ``daijro/geoip-all-in-one`` release tag via the GitHub API.""" - headers = {"Accept": "application/vnd.github+json"} - token = _github_token() - if token: - headers["Authorization"] = f"token {token}" - r = requests.get( - f"https://api.github.com/repos/{GEOIP_REPO}/releases/latest", - headers=headers, timeout=15, - ) - r.raise_for_status() - tag = r.json().get("tag_name") - if not tag: - raise RuntimeError("no tag_name in geoip-all-in-one latest release") - return tag - - -def _download_geoip_tag(tag: str) -> Path: - """Download + extract a specific tag's mmdb if not already cached.""" - dst_dir = _geoip_root() / tag - target = dst_dir / GEOIP_MMDB_NAME - if not target.exists(): - url = GEOIP_RELEASE_URL_TEMPLATE.format(tag=tag, asset=GEOIP_ASSET) - dst_dir.mkdir(parents=True, exist_ok=True) - with tempfile.TemporaryDirectory() as td: - archive = Path(td) / GEOIP_ASSET - _download_file(url, archive) - _extract(archive, dst_dir) - if target.exists(): - return target - # asset name inside the zip may differ from GEOIP_MMDB_NAME - found = sorted(dst_dir.glob("*.mmdb")) - if found: - return found[0] - raise RuntimeError(f"geoip mmdb not found after extraction in {dst_dir}") - - -def _prune_old_geoip_tags(keep: str) -> None: - """Drop every cached tag dir except ``keep`` to bound disk usage.""" - root = _geoip_root() - if not root.exists(): - return - for d in root.iterdir(): - if d.is_dir() and d.name != keep: - shutil.rmtree(d, ignore_errors=True) - - -def geoip_mmdb_path() -> Path | None: - """Path to the currently-cached mmdb (newest tag), or None if none cached.""" - return _cached_geoip_mmdb() - - -def ensure_geoip_mmdb(max_age_days: int = GEOIP_REFRESH_DAYS) -> Path: - """Return a geoip mmdb, kept fresh against daijro's weekly rebuild. - - Resolution order: - 1. ``STEALTHFOX_GEOIP_MMDB`` env → use that file (user-supplied / test). - 2. A cached mmdb younger than ``max_age_days`` → use it (no network). - 3. Else ask GitHub for the latest tag, download it if not already cached, - prune older tags, and reset the freshness timer. - 4. If the API/download is unreachable but a cached mmdb exists → use it - (and reset the timer so we don't hammer the API while offline). - 5. Cold cache + no network → fall back to the pinned ``GEOIP_MMDB_VERSION``; - if that download also fails, raise. - """ - override = os.environ.get("STEALTHFOX_GEOIP_MMDB") - if override: - p = Path(override) - if not p.exists(): - raise RuntimeError(f"STEALTHFOX_GEOIP_MMDB points to a missing file: {p}") - return p - - cached = _cached_geoip_mmdb() - if cached and _geoip_cache_fresh(max_age_days): - return cached - - try: - tag = _latest_geoip_tag() - except Exception: - if cached: - _touch_geoip_marker() # recheck after the window; don't hammer - return cached - tag = GEOIP_MMDB_VERSION # cold cache + API down → pinned fallback - - mmdb = _download_geoip_tag(tag) - _prune_old_geoip_tags(mmdb.parent.name) - _touch_geoip_marker() - return mmdb diff --git a/src/invisible_playwright/launcher.py b/src/invisible_playwright/launcher.py index bcee7ae..b79e4ff 100644 --- a/src/invisible_playwright/launcher.py +++ b/src/invisible_playwright/launcher.py @@ -2,14 +2,12 @@ from __future__ import annotations import secrets -from pathlib import Path from typing import Any, Dict, Optional, Union -from playwright.sync_api import Browser, BrowserContext, Playwright, sync_playwright +from playwright.sync_api import Browser, Playwright, sync_playwright from ._fpforge import Profile, generate_profile -from ._geo import prepare_session_geo -from ._headless import cloak_prefs, make_virtual_display +from ._headless import make_virtual_display from ._proxy import configure_proxy as _configure_proxy_shared from .download import ensure_binary from .prefs import translate_profile_to_prefs @@ -113,8 +111,6 @@ class InvisiblePlaywright: timezone: str = "", extra_prefs: Optional[Dict[str, Any]] = None, binary_path: Optional[str] = None, - profile_dir: Optional[Union[str, Path]] = None, - prep_recaptcha: bool = False, ) -> None: """ Args: @@ -136,26 +132,11 @@ class InvisiblePlaywright: a float caps the motion in seconds. locale: BCP-47 tag (e.g. ``"en-US"``). Drives the ``Accept-Language`` header and ``navigator.language``. - timezone: IANA zone (e.g. ``"America/New_York"``) — used as-is - when set, the only way to force a specific zone. ``""`` - (default) or ``"auto"`` ALWAYS resolves from the egress IP: - through the proxy when one is set, otherwise from the host's - own public IP (one lookup + an offline mmdb). On failure: with - a proxy it raises (a foreign proxy on the host TZ is the - ``timezone_mismatch`` signal); without a proxy it falls back to - the host TZ so a transient lookup failure can't break launch. + timezone: IANA timezone (e.g. ``"America/New_York"``). Empty + means use the host TZ. extra_prefs: Optional dict of Firefox prefs overlayed on top of the generated profile — useful for niche tweaks without monkey-patching the package. - profile_dir: Path to a persistent Firefox profile directory. - When set, the session uses ``launch_persistent_context()`` - so cookies, localStorage, sessionStorage, extensions, cache - and prefs are kept on disk between runs. ``__enter__`` - returns a ``BrowserContext`` (not a ``Browser``) — use it - directly: ``with InvisiblePlaywright(profile_dir=p) as ctx: - page = ctx.new_page()``. First run creates the dir; - subsequent runs reuse it. Pair with a stable ``seed=`` to - also pin the fingerprint identity across runs. """ # Constrain to int31 — Firefox's `zoom.stealth.fpp.hw_seed` and # related stealth prefs are declared as ``int32_t`` in @@ -173,29 +154,12 @@ class InvisiblePlaywright: self._timezone = timezone self._extra_prefs = extra_prefs self._binary_path = binary_path - self._profile_dir: Optional[Path] = Path(profile_dir) if profile_dir else None - # reCAPTCHA cookie pre-seed — opt-in. Gated server-side: if a - # persistent profile_dir is in use, respect its existing cookies - # and DON'T enable pre-seed (the profile owns its own state). - self._prep_recaptcha = bool(prep_recaptcha) and self._profile_dir is None self._profile: Profile = generate_profile(self.seed, pin=self._pin) self._pw: Optional[Playwright] = None self._browser: Optional[Browser] = None - self._persistent_context: Optional[BrowserContext] = None self._virtual_display: Any = None - # Proxy egress IP, discovered at launch (see __enter__). Feeds the - # WebRTC srflx override so the candidate matches the proxy IP, not the - # real host IP. None when no proxy is set. - self._webrtc_egress_ip: Optional[str] = None - def __enter__(self) -> Union[Browser, BrowserContext]: - # Resolve timezone="auto" (and the proxy-set-but-unset default) to a - # concrete IANA zone AND discover the proxy egress IP — one round-trip, - # before anything reads self._timezone or builds prefs/env. Fail-early - # if a proxy is set but the egress can't be resolved. - _geo = prepare_session_geo(self._timezone, self._proxy) - self._timezone = _geo.timezone - self._webrtc_egress_ip = _geo.egress_ip + def __enter__(self) -> Browser: executable = self._binary_path or ensure_binary() prefs = self._build_prefs() playwright_proxy = _configure_proxy_shared(self._proxy, prefs) @@ -204,25 +168,6 @@ class InvisiblePlaywright: try: self._pw = sync_playwright().start() - if self._profile_dir is not None: - # Persistent context — cookies / localStorage / extensions / - # prefs all live on disk between runs. Stealth prefs are - # re-injected via firefox_user_prefs on every launch (Playwright - # writes them to user.js, which overrides anything in - # prefs.js inside the persistent dir). - self._profile_dir.mkdir(parents=True, exist_ok=True) - self._persistent_context = self._pw.firefox.launch_persistent_context( - user_data_dir=str(self._profile_dir), - executable_path=str(executable), - headless=pw_headless, - firefox_user_prefs=prefs, - proxy=playwright_proxy, - args=self._extra_args, - env=env, - **self._persistent_context_kwargs(), - ) - _patch_sync_new_page_sleep(self._persistent_context) - return self._persistent_context self._browser = self._pw.firefox.launch( executable_path=str(executable), headless=pw_headless, @@ -240,22 +185,6 @@ class InvisiblePlaywright: self._patch_new_context_defaults(self._browser) return self._browser - def _persistent_context_kwargs(self) -> Dict[str, Any]: - """Context-level kwargs accepted by launch_persistent_context. - - Identical to ``_default_context_kwargs``: viewport / screen / DPR / - color-scheme / locale / timezone_id. Up to firefox-4 we had to drop - locale and timezone_id because Playwright's per-realm overrides - called IDL methods (``docShell.languageOverride``, - ``docShell.overrideTimezone``) that weren't exposed by our patched - build, causing launch_persistent_context to hang for 180s. From - firefox-5 (C7 chiusura), the C++ ``overrideTimezone`` method is - present and ``languageOverride`` was already there, so the - per-realm overrides land and the persistent context starts in - ~20s like the non-persistent path. - """ - return self._default_context_kwargs() - def _patch_new_context_defaults(self, browser: Browser) -> None: """Wrap ``browser.new_context`` so its defaults derive from the profile (viewport, screen, DPR, color-scheme). Users get a @@ -263,18 +192,12 @@ class InvisiblePlaywright: """ original = browser.new_context defaults = self._default_context_kwargs() - prep = self._prep_recaptcha - profile = self._profile # pass the whole Profile (seed + browsing_history) - tz = self._timezone # used by _recaptcha_seed for CONSENT lang+region def patched(**kw): merged = dict(defaults) merged.update(kw) # user-supplied wins ctx = original(**merged) _patch_sync_new_page_sleep(ctx) - if prep: - from ._recaptcha_seed import seed_recaptcha_cookies_sync - seed_recaptcha_cookies_sync(ctx, profile, timezone=tz) return ctx browser.new_context = patched # type: ignore[assignment] @@ -303,12 +226,6 @@ class InvisiblePlaywright: self._teardown() def _teardown(self) -> None: - if self._persistent_context is not None: - try: - self._persistent_context.close() - except Exception: - pass - self._persistent_context = None if self._browser is not None: try: self._browser.close() @@ -340,19 +257,9 @@ class InvisiblePlaywright: extra_prefs=self._extra_prefs, virtual_display=bool(self._headless and _sys.platform == "win32"), ) - # Windows & macOS hide the headless window via the binary's own cloak - # (DWMWA_CLOAK / NSWindow alpha) — inject the pref so the patched build - # cloaks its chrome windows. setdefault: an explicit user override wins. - if self._headless and _sys.platform in ("win32", "darwin"): - for _k, _v in cloak_prefs().items(): - prefs.setdefault(_k, _v) - # Pref namespace MUST be stealthfox.* — that's what the binary's Juggler - # reads (PageHandler.js gates the Bezier mouse path on `stealthfox.humanize`). - # The old `invisible_playwright.*` name was a dead no-op (nothing read it), so - # humanize silently never fired and every click teleported the cursor. - prefs["stealthfox.humanize"] = bool(self._humanize) + prefs["invisible_playwright.humanize"] = bool(self._humanize) if self._humanize: - prefs["stealthfox.humanize.maxTime"] = str(self._humanize_max_seconds()) + prefs["invisible_playwright.humanize.maxTime"] = str(self._humanize_max_seconds()) return prefs def _build_env(self) -> Dict[str, str]: @@ -371,36 +278,26 @@ class InvisiblePlaywright: env = _os.environ.copy() if self._timezone: env["TZ"] = _tz_env(self._timezone) - # WebRTC srflx override: feed nICEr's nr_stealth_bridge the proxy egress - # IP so the srflx candidate matches the proxy (not the real host the - # UDP STUN would otherwise leak). An explicit env var set by the caller - # wins; otherwise we use the egress IP auto-discovered in __enter__. - # Behind a proxy we also drop IPv6 from gathering (the disableIPv6 pref - # is dead on FF150 — the bridge filter is the real switch). - webrtc_ip = ( - _os.environ.get("STEALTHFOX_WEBRTC_PUBLIC_IP") - or self._webrtc_egress_ip - ) - if webrtc_ip: - env["STEALTHFOX_WEBRTC_PUBLIC_IP"] = webrtc_ip - env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] = "1" + # Propagate STEALTHFOX_WEBRTC_PUBLIC_IP if the process set it — read + # by nICEr's nr_stealth_bridge to inject a synthetic srflx candidate + # matching the proxy egress IP. This avoids the StaticPref IPC + # propagation timing issue between parent and socket processes. + if _os.environ.get("STEALTHFOX_WEBRTC_PUBLIC_IP"): + env["STEALTHFOX_WEBRTC_PUBLIC_IP"] = _os.environ["STEALTHFOX_WEBRTC_PUBLIC_IP"] return env def _resolve_headless(self) -> bool: """Translate the user's ``headless`` flag. - When ``True``, Firefox stays in headed mode (real rendering pipeline → - coherent fingerprint) and the window is hidden: on Linux via a fresh - Xvfb spawned here; on Windows/macOS via the binary's own window cloak - (the ``zoom.stealth.cloak_windows`` pref added in ``_build_prefs``), so - ``make_virtual_display()`` returns ``None`` and nothing is spawned. + When ``True``, we keep Firefox in headed mode (real rendering + pipeline → coherent fingerprint) and hide the windows on a fresh + Xvfb (Linux) or hidden Windows desktop. """ if not self._headless: return False vd = make_virtual_display() - if vd is not None: - vd.start() - self._virtual_display = vd + vd.start() + self._virtual_display = vd return False def _humanize_max_seconds(self) -> float: diff --git a/src/invisible_playwright/prefs.py b/src/invisible_playwright/prefs.py index 96851f7..43ece27 100644 --- a/src/invisible_playwright/prefs.py +++ b/src/invisible_playwright/prefs.py @@ -208,21 +208,15 @@ _BASELINE: Dict[str, Any] = { "privacy.fingerprintingProtection.pbmode": False, "privacy.fingerprintingProtection.remoteOverrides.enabled": False, - # WebRTC: enabled, looks like a real Firefox behind NAT, no real-IP leak. - # obfuscate_host_addresses=true → host candidate is `.local` mDNS, - # exactly like vanilla Firefox (BrowserLeaks "No Leak", Local IP "-"). - # The mDNS-IPC hang feared on older builds does NOT reproduce on FF150. - # The proxy-egress srflx is injected by our C++ (srflx swap §17 + fallback - # §17.B), fed the egress IP via STEALTHFOX_WEBRTC_PUBLIC_IP from - # launcher._build_env (auto-discovered from the proxy). - # IPv6: media.peerconnection.ice.disableIPv6 is DEAD on FF150 (read by no - # ICE-gathering code). The real switch is our zoom.stealth.webrtc.disable_ipv6 - # (nICEr addrs.cpp filter) + the STEALTHFOX_WEBRTC_DISABLE_IPV6 env. + # WebRTC: enabled, no public IP leak. + # obfuscate_host_addresses=false: our C++ injection handles candidate + # selection; mDNS causes mDNS-IPC to hang in sandboxed content processes. + # disableIPv6=true keeps IPv6 out of gathering (less entropy, no IPv6 leak). "media.peerconnection.enabled": True, "media.peerconnection.ice.no_host": False, "media.peerconnection.ice.default_address_only": False, - "media.peerconnection.ice.obfuscate_host_addresses": True, - "zoom.stealth.webrtc.disable_ipv6": True, + "media.peerconnection.ice.obfuscate_host_addresses": False, + "media.peerconnection.ice.disableIPv6": True, "media.peerconnection.ice.proxy_only": False, "media.peerconnection.ice.relay_only": False, "media.peerconnection.use_document_iceservers": True, @@ -231,17 +225,6 @@ _BASELINE: Dict[str, Any] = { "network.proxy.socks_remote_dns": True, "network.proxy.failover_direct": False, - # TLS ClientHello fingerprint — match stock Firefox byte-for-byte. - # The Playwright/Juggler Firefox build this binary derives from re-enables - # cipher 0xC009 (TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA), which retail Firefox - # 150 does NOT offer. That extra (17th) cipher shifts our JA3/JA4 away from - # any real Firefox (ja4 t13d1717h2 vs stock t13d1617h2). A ClientHello that - # matches no real browser is itself a consistency tell. Disabling it makes - # JA3/JA4/peetprint byte-identical to retail FF150 (verified on tls.peet.ws). - # Stock Firefox ships without 0xC009 and works on the whole web, so this only - # improves fingerprint consistency — it cannot break connectivity. - "security.ssl3.ecdhe_ecdsa_aes_128_sha": False, - # Safebrowsing — chatty and fingerprintable. "browser.safebrowsing.malware.enabled": False, "browser.safebrowsing.phishing.enabled": False, @@ -306,29 +289,13 @@ _BASELINE: Dict[str, Any] = { "network.dns.echconfig.enabled": False, "network.dns.use_https_rr_as_altsvc": False, - # === Fission / site-isolation disabled (FF146 Playwright parity) === - # Force a single content-process model. Three knobs are required in FF150: - # upstream Playwright Firefox (FF146-based) only needed fission.autostart=False - # because FF146's default isolation strategy was looser. FF150 ships with - # fission.webContentIsolationStrategy=1 (IsolateEverything) which still - # site-isolates cross-origin iframes into separate `webIsolated` content - # processes EVEN WHEN fission.autostart is False. From the parent process's - # point of view, those iframes get a Juggler Frame placeholder with no - # docShell, no URL, and an execution context that wraps the wrong global, - # so frame.evaluate() fails with cross-origin SOP errors and - # element_handle.content_frame() returns None. - # - # Pinning the strategy to 0 keeps every cross-origin web iframe in the - # parent's content process, where the Juggler code paths from the FF146 - # era expect them. processCount.webIsolated=1 is kept as belt-and-suspenders - # in case some path still classifies an origin as webIsolated despite the - # strategy change. It costs nothing to leave. - # - # See issue #20 + tests/test_cross_origin_iframe.py for the regression - # sentinel that catches a future A/B flipping these back. + # === A/B VARIANT B: Fission disabled === + # Force single content-process model (e10s only, no BC outer/inner split). + # Diagnostic for the FF150 BC-swap theory: if peet_ws/fppro/sannysoft + # work with this off, the Juggler FF146 baseline breaks specifically on + # cross-process navigation tracking. "fission.autostart": False, "fission.autostart.session": False, - "fission.webContentIsolationStrategy": 0, # IsolateNothing "dom.ipc.processCount.webIsolated": 1, @@ -417,21 +384,6 @@ _WIN_VIRT_DESKTOP_WORKAROUNDS: Dict[str, Any] = { # Bugzilla refs: 1798091, 1524591, 1229829. Lowering the GPU sandbox to 0 # restores hardware compositor + functional WebGL on alt desktops. "security.sandbox.gpu.level": 0, - # Same root cause as above, content process side. Wrapper repo issue #18 - # (tab crash on cross-process navigation under headless=True). Sandbox - # content level > 4 puts content processes on the sandbox's own - # kAlternateWinstation (see security/sandbox/win/src/sandboxbroker/ - # sandboxBroker.cpp line 1113-1114: - # `if (aSandboxLevel > 4) config->SetDesktop(kAlternateWinstation)`). - # Combined with our CreateDesktop alt-desktop, that puts browser process - # and content processes on DIFFERENT desktops. Cross-process navigation - # then fails window parenting between parent and child, the content - # process exits cleanly (exitCode=0, signal=null) and Playwright fires - # page.on('crash') ~10s after page load. Lowering content sandbox to 4 - # keeps content processes on the same desktop as the browser process, - # which is what we want here (still tight enough — level 4 blocks - # file/registry write, network calls, hardware access). - "security.sandbox.content.level": 4, } @@ -568,17 +520,12 @@ def translate_profile_to_prefs( prefs["privacy.spoof_english"] = 0 if timezone: - # juggler.timezone.override is the SOLE source of truth read by the C++ - # timezone chain (BrowsingContext::Attach/DidSet, ContentChild). The old - # zoom.stealth.timezone pref was declared in the yaml but read by NO - # code — dropped here on 2026-06-10 (see 20-our-patches.md §8). + prefs["zoom.stealth.timezone"] = timezone prefs["juggler.timezone.override"] = timezone - # Cross-process seed (canvas noise + DWrite gamma share this). Only - # zoom.stealth.fpp.hw_seed is read by the C++; the old zoom.stealth.seed - # alias was never declared in the yaml and read by nothing — dropped - # 2026-06-10. + # Cross-process seed (canvas noise + DWrite gamma share this). prefs["zoom.stealth.fpp.hw_seed"] = profile.seed + prefs["zoom.stealth.seed"] = profile.seed # Synthetic host ICE candidate — injected by C++ when addr_ct==0 (SOCKS5 # proxy suppresses all local addresses so Firefox can't gather host cands). diff --git a/tests/conftest.py b/tests/conftest.py index 900732b..429aa6d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,12 +1,8 @@ -import os import random -import sys -from pathlib import Path import pytest from invisible_playwright._fpforge import generate_profile -from invisible_playwright.constants import BINARY_ENTRY_REL @pytest.fixture @@ -19,36 +15,3 @@ def deterministic_rng(): def sample_profile(): """A Profile generated from seed=42 for reuse across tests.""" return generate_profile(seed=42) - - -@pytest.fixture(scope="session") -def firefox_binary(): - """Locate the patched Firefox binary for E2E tests, or skip cleanly. - - Single source of truth for every E2E test (previously each test file had its - own copy — and three of them silently ignored INVPW_BINARY_PATH, so they kept - testing whatever was in the cache even when you pointed the suite at a - specific build: a false-confidence trap). Lookup order: - - 1. ``INVPW_BINARY_PATH`` env var — point the whole suite at a local build - or a freshly-extracted release (this is how the full-suite gate runs). - 2. Cached binary under ``cache_dir_for_version()`` (post ``fetch``). - 3. Skip — we never trigger an implicit multi-hundred-MB network download - inside a test run. - """ - env_path = os.environ.get("INVPW_BINARY_PATH") - if env_path: - if Path(env_path).exists(): - return env_path - pytest.skip(f"INVPW_BINARY_PATH={env_path!r} does not exist") - - if sys.platform not in BINARY_ENTRY_REL: - pytest.skip(f"unsupported platform: {sys.platform}") - from invisible_playwright.download import cache_dir_for_version - entry = cache_dir_for_version() / BINARY_ENTRY_REL[sys.platform] - if not entry.exists(): - pytest.skip( - "patched Firefox binary not cached and INVPW_BINARY_PATH unset; " - "set INVPW_BINARY_PATH= or run `invisible-playwright fetch`" - ) - return str(entry) diff --git a/tests/test_cloak.py b/tests/test_cloak.py deleted file mode 100644 index 71ad50a..0000000 --- a/tests/test_cloak.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Cloak guard (e2e) — verifies the source-level "invisible headless" cloak: -the chrome window is hidden from the screen YET keeps rendering on the real GPU -(not Playwright's native headless, which has no WebGL). Runs per-platform in CI: -- Windows: the DWMWA_CLOAK attribute (queried via DWMWA_CLOAKED). -- macOS: the NSWindow alpha (queried via Quartz CGWindowListCopyWindowInfo). -- Linux: skipped — there the wrapper hides via Xvfb, not a source-level cloak. - -This is the CI validation for the macOS cocoa cloak patch, which can't be built -or run on the Windows/Linux dev boxes. -""" -from __future__ import annotations - -import sys -import time - -import pytest - -from invisible_playwright import InvisiblePlaywright - -CLOAK_PREFS = { - "zoom.stealth.cloak_windows": True, - "widget.windows.window_occlusion_tracking.enabled": False, -} - -_WEBGL_RENDERER = """() => { - const g = document.createElement('canvas').getContext('webgl'); - if (!g) return 'NO-WEBGL'; - const d = g.getExtension('WEBGL_debug_renderer_info'); - return d ? g.getParameter(d.UNMASKED_RENDERER_WEBGL) : (g.getParameter(g.RENDERER) || ''); -}""" - - -def _windows_moz_window_cloaked() -> bool: - """True if at least one MozillaWindowClass top-level window is DWMWA_CLOAKED.""" - import ctypes - from ctypes import wintypes - - user32 = ctypes.windll.user32 - dwm = ctypes.windll.dwmapi - DWMWA_CLOAKED = 14 - ENUM = ctypes.WINFUNCTYPE(wintypes.BOOL, wintypes.HWND, wintypes.LPARAM) - found = [] - - def cb(hwnd, _): - c = ctypes.create_unicode_buffer(256) - user32.GetClassNameW(hwnd, c, 256) - if c.value == "MozillaWindowClass": - v = wintypes.DWORD(0) - dwm.DwmGetWindowAttribute(wintypes.HWND(hwnd), DWMWA_CLOAKED, - ctypes.byref(v), 4) - found.append(v.value) - return True - - user32.EnumWindows(ENUM(cb), 0) - return any(state != 0 for state in found) - - -def _macos_firefox_window_alpha_zero() -> bool: - """True if a Firefox on-screen window reports ~0 alpha (cloaked).""" - from Quartz import ( # type: ignore - CGWindowListCopyWindowInfo, - kCGWindowListOptionOnScreenOnly, - kCGNullWindowID, - ) - - infos = CGWindowListCopyWindowInfo(kCGWindowListOptionOnScreenOnly, kCGNullWindowID) - alphas = [] - for w in infos or []: - owner = (w.get("kCGWindowOwnerName") or "") - if "firefox" in owner.lower() or "nightly" in owner.lower(): - alphas.append(float(w.get("kCGWindowAlpha", 1.0))) - # cloaked windows are alpha 0; if Firefox has any window it must be ~0. - return bool(alphas) and all(a < 0.05 for a in alphas) - - -@pytest.mark.e2e -@pytest.mark.skipif( - sys.platform.startswith("linux"), - reason="source-level cloak is Windows/macOS only; Linux hides via Xvfb", -) -def test_cloak_hides_window_but_keeps_rendering(firefox_binary): - with InvisiblePlaywright( - seed=42, binary_path=firefox_binary, headless=False, extra_prefs=CLOAK_PREFS - ) as browser: - page = browser.new_context().new_page() - page.goto("https://example.com", timeout=30_000) - time.sleep(2) - - # 1) still renders on the real GPU pipeline (a non-blank screenshot proves - # the compositor is alive despite the window being hidden). - shot = page.screenshot() - assert len(shot) > 3000, "cloaked window produced a blank screenshot (rendering paused)" - - # 2) headed pipeline intact: a real WebGL context (Playwright's native - # headless has none). Linux (Xvfb + llvmpipe) and Windows (WARP) give a - # software context on the GPU-less runners, so a missing context there - # is a real regression -> hard fail. macOS GitHub runners expose NO - # WebGL in the CI session at all (even vanilla Firefox), and macOS has - # no software-GL fallback; the cloak's "still rendering" property is - # already proven by the non-blank screenshot above, so we don't also - # require a live WebGL context there. - renderer = page.evaluate(_WEBGL_RENDERER) - webgl_ok = bool(renderer) and renderer != "NO-WEBGL" - if not (sys.platform == "darwin" and not webgl_ok): - assert webgl_ok, f"no real WebGL under cloak: {renderer!r}" - - # 3) the window is actually hidden (per-platform). - if sys.platform == "win32": - assert _windows_moz_window_cloaked(), "Firefox window is not DWMWA_CLOAKED" - elif sys.platform == "darwin": - try: - hidden = _macos_firefox_window_alpha_zero() - except ImportError: - pytest.skip("pyobjc Quartz not available to verify macOS cloak alpha") - assert hidden, "Firefox macOS window is not alpha-cloaked" diff --git a/tests/test_constants.py b/tests/test_constants.py index 911ad70..8d124a7 100644 --- a/tests/test_constants.py +++ b/tests/test_constants.py @@ -5,26 +5,11 @@ from invisible_playwright.constants import ( BINARY_BASENAME, BINARY_ENTRY_REL, BINARY_VERSION, - BROKEN_VERSIONS, FIREFOX_UPSTREAM_VERSION, RELEASE_URL_TEMPLATE, ) -@pytest.mark.unit -def test_broken_versions_excludes_current(): - """The current BINARY_VERSION must NEVER be in BROKEN_VERSIONS — otherwise - every default ensure_binary() call would raise and the wrapper is unusable.""" - assert BINARY_VERSION not in BROKEN_VERSIONS - - -@pytest.mark.unit -def test_firefox_8_is_marked_broken(): - """firefox-8 shipped without the juggler layer (undrivable by Playwright); - it must stay flagged so a stale cache can't silently hand it to a user.""" - assert "firefox-8" in BROKEN_VERSIONS - - @pytest.mark.unit def test_binary_version_format(): assert BINARY_VERSION.startswith("firefox-") @@ -46,16 +31,9 @@ def test_archive_name_linux(): @pytest.mark.unit -def test_archive_name_macos_arm64(): - name = ARCHIVE_NAME("darwin", "arm64") - assert name.endswith(".tar.gz") - assert "macos-arm64" in name - - -@pytest.mark.unit -def test_archive_name_truly_unsupported_raises(): +def test_archive_name_unsupported_raises(): with pytest.raises(NotImplementedError): - ARCHIVE_NAME("plan9", "x86_64") + ARCHIVE_NAME("darwin", "arm64") @pytest.mark.unit @@ -99,18 +77,20 @@ def test_archive_name_rejects_unsupported_arches(machine): @pytest.mark.unit @pytest.mark.parametrize("machine", ["arm64", "aarch64"]) -def test_archive_name_arm64_supported(machine): - """ARM64 is shipped now (issue #6): both Linux aarch64 and macOS arm64. - ARCHIVE_NAME must map both machine spellings to the canonical -arm64 asset.""" - assert ARCHIVE_NAME("linux", machine) == "firefox-150.0.1-stealth-linux-arm64.tar.gz" - assert ARCHIVE_NAME("darwin", machine) == "firefox-150.0.1-stealth-macos-arm64.tar.gz" +def test_archive_name_arm64_not_yet_supported(machine): + """ARM64 is a frequent request (issue #6). Until binaries exist for it, + ARCHIVE_NAME should hard-fail rather than silently degrade. If this test + starts failing because someone shipped ARM64 builds, replace it with the + positive case.""" + with pytest.raises(NotImplementedError): + ARCHIVE_NAME("linux", machine) @pytest.mark.unit -@pytest.mark.parametrize("platform_key", ["freebsd", "cygwin", "openbsd"]) +@pytest.mark.parametrize("platform_key", ["darwin", "freebsd", "cygwin", "openbsd"]) def test_archive_name_rejects_unsupported_platforms(platform_key): - """win32/linux/darwin are supported; other platforms must raise, not - silently pick one of the three.""" + """Same logic — non-Linux/non-Windows platforms must raise, not silently + pick one of the two.""" with pytest.raises(NotImplementedError, match=platform_key): ARCHIVE_NAME(platform_key, "x86_64") @@ -124,7 +104,7 @@ def test_archive_name_rejects_unsupported_platforms(platform_key): def test_binary_entry_rel_covers_every_supported_platform(): """If ARCHIVE_NAME accepts a platform key, BINARY_ENTRY_REL must declare where the executable lives inside the archive for it.""" - for plat in ["win32", "linux", "darwin"]: + for plat in ["win32", "linux"]: ARCHIVE_NAME(plat, "x86_64") # must not raise assert plat in BINARY_ENTRY_REL, ( f"ARCHIVE_NAME accepts {plat!r} but BINARY_ENTRY_REL has no entry " @@ -138,7 +118,6 @@ def test_binary_entry_rel_extension_matches_platform(): assert BINARY_ENTRY_REL["win32"].endswith(".exe") assert not BINARY_ENTRY_REL["linux"].endswith(".exe") assert BINARY_ENTRY_REL["linux"] == "firefox" - assert BINARY_ENTRY_REL["darwin"].endswith(".app/Contents/MacOS/firefox") # ---- RELEASE_URL_TEMPLATE shape ------------------------------------------- # diff --git a/tests/test_cross_origin_iframe.py b/tests/test_cross_origin_iframe.py deleted file mode 100644 index 26df483..0000000 --- a/tests/test_cross_origin_iframe.py +++ /dev/null @@ -1,278 +0,0 @@ -"""Regression tests for cross-origin / cross-process iframe interaction. - -History: wrapper repo issue #20 reported that a third-party cookie -consent iframe was completely unreachable from Playwright in 0.1.7 — -``element_handle.content_frame()`` returned ``None``, ``frame.evaluate()`` -threw cross-origin SOP errors, and ``frame_locator().click()`` timed -out. - -Root cause was a missing pref. FF150 ships with -``fission.webContentIsolationStrategy=1`` (IsolateEverything), which -site-isolates cross-origin iframes into separate webIsolated content -processes even when ``fission.autostart=False``. The Juggler code paths -inherited from the FF146 era assume same-process iframes. The wrapper's -``_BASELINE`` now pins the pref to 0 (IsolateNothing). - -These tests exist so a future Firefox upgrade or a fingerprint A/B -that flips this pref by accident cannot ship without a red CI signal. - -Layers: - * ``unit`` — ``_BASELINE`` contains the pref with the right value. No browser. - * ``e2e`` — launch the real binary against a LOCAL HTTP harness on - ``127.0.0.1`` (two ports = two SOP origins) and verify the - four protocol operations that regressed: frame URL tracking, - ``handle.content_frame()``, ``frame.evaluate()``, and - ``frame_locator(...).locator(...)`` element resolution. - -The e2e tests run entirely offline. They never call out to a real site; -the cross-origin shape is reproduced with two local HTTP servers on -random free ports. -""" -from __future__ import annotations - -import socket -import threading -from http.server import BaseHTTPRequestHandler, HTTPServer - -import pytest - -from invisible_playwright._fpforge import generate_profile -from invisible_playwright.prefs import _BASELINE, translate_profile_to_prefs - - -# ──────────────────────────────────────────────────────────────────── -# Unit layer — fast, no browser, runs on every CI -# ──────────────────────────────────────────────────────────────────── - - -@pytest.mark.unit -def test_baseline_pins_web_content_isolation_strategy_to_zero(): - """Regression sentinel. - - ``fission.webContentIsolationStrategy`` MUST be 0 (IsolateNothing). - The FF150 default is 1 (IsolateEverything), which site-isolates - cross-origin iframes into separate webIsolated content processes - and breaks Playwright frame tracking from the parent process. - """ - assert _BASELINE["fission.webContentIsolationStrategy"] == 0, ( - "fission.webContentIsolationStrategy must be 0 (IsolateNothing). " - "If you bumped it for an A/B, cross-origin iframes will appear " - "in page.frames with empty URLs and content_frame() will return " - "None — see the changelog entry that introduced this test." - ) - - -@pytest.mark.unit -def test_baseline_keeps_fission_autostart_off(): - """Belt for the suspenders above. All three prefs are required.""" - assert _BASELINE["fission.autostart"] is False - assert _BASELINE["fission.autostart.session"] is False - assert _BASELINE["dom.ipc.processCount.webIsolated"] == 1 - - -@pytest.mark.unit -def test_translated_profile_propagates_isolation_strategy(): - """The fix must survive translate_profile_to_prefs, not just live in _BASELINE.""" - p = generate_profile(seed=42) - prefs = translate_profile_to_prefs(p) - assert prefs["fission.webContentIsolationStrategy"] == 0 - - -@pytest.mark.unit -def test_extra_prefs_override_can_break_isolation_only_explicitly(): - """If a caller wants to A/B isolation, they have to set it explicitly. - The wrapper does not silently flip it back on. - """ - p = generate_profile(seed=42) - prefs_default = translate_profile_to_prefs(p) - assert prefs_default["fission.webContentIsolationStrategy"] == 0 - - prefs_ab = translate_profile_to_prefs( - p, extra_prefs={"fission.webContentIsolationStrategy": 1} - ) - assert prefs_ab["fission.webContentIsolationStrategy"] == 1 - - -# ──────────────────────────────────────────────────────────────────── -# E2E layer — needs cached binary + bind to localhost ports -# ──────────────────────────────────────────────────────────────────── - - -def _free_port() -> int: - s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - s.bind(("127.0.0.1", 0)) - port = s.getsockname()[1] - s.close() - return port - - -class _SilentHandler(BaseHTTPRequestHandler): - """Suppress per-request access logging so pytest output stays clean.""" - PAYLOAD = b"" # set per-instance via subclassing - - def log_message(self, *_a): - pass - - def do_GET(self): - self.send_response(200) - self.send_header("Content-Type", "text/html; charset=utf-8") - self.send_header("Cache-Control", "no-store") - self.end_headers() - self.wfile.write(self.PAYLOAD) - - -def _serve(payload: bytes, port: int) -> HTTPServer: - """Start an HTTP server on 127.0.0.1:port serving ``payload`` on every GET.""" - handler_cls = type( - "_H", (_SilentHandler,), {"PAYLOAD": payload} - ) - srv = HTTPServer(("127.0.0.1", port), handler_cls) - t = threading.Thread(target=srv.serve_forever, daemon=True) - t.start() - return srv - - -@pytest.fixture -def cross_origin_harness(): - """Spin up TWO local HTTP servers on different localhost ports. - - Two ports = two distinct origins under SOP (same host, different port - → different origin). The parent page on port A embeds an iframe with - src pointing at port B. Same cross-origin browsing-context shape as - a parent-page-plus-third-party-iframe layout, fully offline. - """ - pa, pb = _free_port(), _free_port() - parent_html = f"""parent -

parent

- - - -""".encode("utf-8") - child_html = b""" - - - -""" - sa = _serve(parent_html, pa) - sb = _serve(child_html, pb) - try: - yield {"parent_url": f"http://127.0.0.1:{pa}/", "child_origin": f"http://127.0.0.1:{pb}"} - finally: - sa.shutdown() - sb.shutdown() - - -@pytest.mark.e2e -def test_cross_origin_iframe_url_appears_in_page_frames(firefox_binary, cross_origin_harness): - """``page.frames`` must list the cross-origin iframe with its real URL. - - Before the pref fix, the URL came back as '' because the navigation - observer for the iframe fired in a different content process than - the parent's FrameTree was registered in. - """ - from invisible_playwright import InvisiblePlaywright - - with InvisiblePlaywright(seed=42, binary_path=firefox_binary, humanize=False) as browser: - ctx = browser.new_context() - page = ctx.new_page() - page.goto(cross_origin_harness["parent_url"], wait_until="domcontentloaded", timeout=30_000) - page.wait_for_selector("iframe#ifr_plain", timeout=10_000) - page.wait_for_timeout(500) - - urls = [f.url for f in page.frames] - assert any(cross_origin_harness["child_origin"] in (u or "") for u in urls), ( - f"no frame had the child origin in its URL; page.frames urls = {urls!r}" - ) - - -@pytest.mark.e2e -def test_cross_origin_iframe_content_frame_resolves(firefox_binary, cross_origin_harness): - """``handle.content_frame()`` must return a Frame (not None) for every - cross-origin iframe shape we care about: plain, sandboxed, titled. - """ - from invisible_playwright import InvisiblePlaywright - - with InvisiblePlaywright(seed=42, binary_path=firefox_binary, humanize=False) as browser: - ctx = browser.new_context() - page = ctx.new_page() - page.goto(cross_origin_harness["parent_url"], wait_until="domcontentloaded", timeout=30_000) - page.wait_for_selector("iframe#ifr_plain", timeout=10_000) - page.wait_for_timeout(500) - - for sel in ("iframe#ifr_plain", "iframe#ifr_sandbox", "iframe#ifr_titled"): - handle = page.query_selector(sel) - assert handle is not None, f"{sel!r} not found in DOM" - cf = handle.content_frame() - assert cf is not None, f"{sel!r}: content_frame() returned None" - assert cross_origin_harness["child_origin"] in (cf.url or ""), ( - f"{sel!r}: content_frame().url = {cf.url!r}, " - f"expected child origin {cross_origin_harness['child_origin']!r}" - ) - - -@pytest.mark.e2e -def test_cross_origin_iframe_evaluate_returns_real_values(firefox_binary, cross_origin_harness): - """``frame.evaluate()`` inside the cross-origin iframe must work. - - Pre-fix: every evaluate failed with a cross-origin SOP error because - the iframe ended up with a stale/wrong execution context. - """ - from invisible_playwright import InvisiblePlaywright - - with InvisiblePlaywright(seed=42, binary_path=firefox_binary, humanize=False) as browser: - ctx = browser.new_context() - page = ctx.new_page() - page.goto(cross_origin_harness["parent_url"], wait_until="domcontentloaded", timeout=30_000) - page.wait_for_selector("iframe#ifr_plain", timeout=10_000) - page.wait_for_timeout(500) - - cf = page.query_selector("iframe#ifr_plain").content_frame() - assert cf is not None - href = cf.evaluate("() => location.href") - assert cross_origin_harness["child_origin"] in href - title = cf.evaluate("() => document.title") - assert isinstance(title, str) - n_buttons = cf.evaluate("() => document.querySelectorAll('button').length") - assert n_buttons == 2 - - -@pytest.mark.e2e -def test_cross_origin_iframe_frame_locator_resolves_button(firefox_binary, cross_origin_harness): - """``frame_locator(...).locator(...)`` must reach the button inside the iframe.""" - from invisible_playwright import InvisiblePlaywright - - with InvisiblePlaywright(seed=42, binary_path=firefox_binary, humanize=False) as browser: - ctx = browser.new_context() - page = ctx.new_page() - page.goto(cross_origin_harness["parent_url"], wait_until="domcontentloaded", timeout=30_000) - page.wait_for_selector("iframe#ifr_plain", timeout=10_000) - - for selector in ("button#ok", "button.btn-primary"): - cnt = page.frame_locator("iframe#ifr_plain").locator(selector).count() - assert cnt == 1, f"locator({selector!r}) found {cnt} elements (expected 1)" - - -@pytest.mark.e2e -def test_cross_origin_iframe_dispatch_event_click_works(firefox_binary, cross_origin_harness): - """End-to-end interaction via ``dispatch_event`` must succeed. - - Plain ``.click()`` can trip Playwright's actionability heuristic on - some third-party UIs (same on vanilla Playwright Firefox — not our - regression), but ``dispatch_event('click')`` always works once the - iframe is reachable. - """ - from invisible_playwright import InvisiblePlaywright - - with InvisiblePlaywright(seed=42, binary_path=firefox_binary, humanize=False) as browser: - ctx = browser.new_context() - page = ctx.new_page() - page.goto(cross_origin_harness["parent_url"], wait_until="domcontentloaded", timeout=30_000) - page.wait_for_selector("iframe#ifr_plain", timeout=10_000) - - page.frame_locator("iframe#ifr_plain").locator("button#ok").dispatch_event( - "click", timeout=4_000 - ) - cf = page.query_selector("iframe#ifr_plain").content_frame() - assert cf.evaluate("() => document.title") == "clicked" diff --git a/tests/test_detectors_e2e.py b/tests/test_detectors_e2e.py deleted file mode 100644 index c37f67c..0000000 --- a/tests/test_detectors_e2e.py +++ /dev/null @@ -1,294 +0,0 @@ -"""E2E: run the REAL open-source detectors against the patched binary, on CI. - -Instead of our own hand-rolled signal checks, this loads the actual detection -libraries and uses their FULL API surface: - - * BotD (@fingerprintjs/botd, MIT) — the client-side bot detector that - FingerprintJS Pro itself uses. We assert the aggregate verdict - (``detect().bot == False``) AND every one of its ~18 individual detectors - (``getDetections()``) returns ``bot == False``. - * FingerprintJS open-source (MIT) — ``get()`` must return a ``visitorId`` - that is STABLE across two fresh launches with the same seed, and a RICH - component set (the fingerprint surface is real, not a stub). - * fpscanner (antoinevastel/fpscanner 1.0.6, MIT) — ``collectFingerprint()`` - runs ~21 bot-detection rules in the browser. We assert the **engine-agnostic** - subset (webdriver / selenium / bot-UA / platform / timezone / language) is - clean. We deliberately do NOT assert the Chrome/GPU-only rules (hasCDP, - hasPlaywright, hasSwiftshaderRenderer, hasMissingChromeObject, …): they're - trivially clean on Firefox, and the GPU ones can legitimately fire on a - software-WebGL CI host (Xvfb/llvmpipe) — asserting them would false-red. - * CreepJS (abrahamjuliot/creepjs, MIT, pinned) — the gold-standard Firefox-aware - headless/stealth/lie detector. It exposes its result on ``window.Fingerprint``. - We assert ``headlessRating == 0`` (webdriver + headless-UA tells) and the - JS-proxy stealth tells are absent. ``stealthRating`` / ``totalLies`` / - ``likeHeadlessRating`` are LOGGED, not hard-asserted, because some of their - sub-signals (hasBadWebGL, prefers-light-color) are GPU/theme-sensitive and - differ on a GPU-less CI host. - -Everything is hermetic: the libraries are vendored (tests/vendor/) and served -from a localhost HTTP server — no external CDN call. For CreepJS, every non-local -request is aborted, so its optional crowd-comparison POST never runs and the -verdict is computed purely locally. Runs identically on a dev box and a GH runner. - -NOT covered: FingerprintJS *Pro* (commercial, server-side) — stays the local -realness gate. -""" -from __future__ import annotations - -import http.server -import socketserver -import threading -from pathlib import Path - -import pytest - -from invisible_playwright import InvisiblePlaywright - -_VENDOR = Path(__file__).parent / "vendor" -_BOTD = "botd-2.0.0.esm.js" -_FPJS = "fingerprintjs-5.2.0.umd.min.js" -_FPSCANNER = "fpscanner-1.0.6.es.js" -_CREEPJS = "creepjs-10aa672.js" # pinned abrahamjuliot/creepjs@10aa6724 - -# fpscanner rules that are MEANINGFUL on Firefox and GPU-independent — these must -# stay clean. The omitted rules are Chrome-only (hasCDP/hasPlaywright/ -# hasMissingChromeObject/hasHighCPUCount/hasImpossibleDeviceMemory/ -# headlessChromeScreenResolution) or GPU-sensitive on a software-WebGL CI host -# (hasSwiftshaderRenderer/hasGPUMismatch/hasMismatchWebGLInWorker). -_FPSCANNER_AGNOSTIC = [ - "hasWebdriver", "hasWebdriverIframe", "hasWebdriverWorker", "hasWebdriverWritable", - "hasSeleniumProperty", "hasBotUserAgent", "hasPlatformMismatch", - "hasMismatchLanguages", "hasUTCTimezone", "hasMismatchPlatformIframe", - "hasMismatchPlatformWorker", "hasInconsistentEtsl", -] - -_PAGE = f""" -detectors - -

loading

-""" - -# CreepJS gets its own page: creep.js is a plain `defer` script that runs on load -# and populates window.Fingerprint. A minimal DOM is enough (the rich report DOM -# is only for the visual page, not the computation). -_CREEP_PAGE = f"""creep -
""" - - -class _DetectorSite: - """Localhost server: `/` → BotD+FPJS+fpscanner page, `/creepjs` → CreepJS page, - `/` → the vendored bundle.""" - - def __init__(self): - page = _PAGE.encode() - creep_page = _CREEP_PAGE.encode() - vendor = _VENDOR - - class H(http.server.BaseHTTPRequestHandler): - def do_GET(self): # noqa: N802 - p = self.path.split("?")[0] - if p == "/": - body, ctype = page, "text/html; charset=utf-8" - elif p == "/creepjs": - body, ctype = creep_page, "text/html; charset=utf-8" - else: - f = vendor / Path(p.lstrip("/")).name - if not f.is_file(): - self.send_error(404); return - body = f.read_bytes() - ctype = "text/javascript; charset=utf-8" - self.send_response(200) - self.send_header("Content-Type", ctype) - self.send_header("Content-Length", str(len(body))) - self.end_headers() - self.wfile.write(body) - - def log_message(self, *a): - pass - - self._srv = socketserver.TCPServer(("127.0.0.1", 0), H) - self.port = self._srv.server_address[1] - threading.Thread(target=self._srv.serve_forever, daemon=True).start() - - @property - def url(self): - return f"http://127.0.0.1:{self.port}/" - - @property - def creep_url(self): - return f"http://127.0.0.1:{self.port}/creepjs" - - def close(self): - self._srv.shutdown() - - -@pytest.fixture(scope="module") -def detector_site(): - s = _DetectorSite() - yield s - s.close() - - -def _run_detectors(firefox_binary, url): - """Launch the binary, load the page, return (botd, fp, fps, err).""" - with InvisiblePlaywright(seed=42, binary_path=firefox_binary) as browser: - page = browser.new_page() - page.goto(url, wait_until="load", timeout=45000) - page.wait_for_function( - "() => document.getElementById('state').textContent === 'done'", - timeout=45000, - ) - botd = page.evaluate("() => window.__botd") - fp = page.evaluate("() => window.__fp") - fps = page.evaluate("() => window.__fps") - err = page.evaluate("() => window.__err") - return botd, fp, fps, err - - -def _run_creepjs(firefox_binary, creep_url): - """Launch the binary, run CreepJS fully offline, return its headless result.""" - _EV = """() => { - const f = window.Fingerprint; - if (!f || !f.headless) return { ready: false }; - const h = f.headless; - return { - ready: true, - headlessRating: h.headlessRating, - stealthRating: h.stealthRating, - likeHeadlessRating: h.likeHeadlessRating, - headless: h.headless || {}, - stealth: h.stealth || {}, - totalLies: (f.lies && f.lies.totalLies) || 0, - }; - }""" - with InvisiblePlaywright(seed=42, binary_path=firefox_binary) as browser: - page = browser.new_page() - # truly offline: abort every non-loopback request (CreepJS's optional - # crowd-comparison POST to arh.antoinevastel.com never runs). - page.route( - "**/*", - lambda r: r.abort() if "127.0.0.1" not in r.request.url else r.continue_(), - ) - page.goto(creep_url, wait_until="domcontentloaded", timeout=45000) - page.wait_for_function( - "() => !!(window.Fingerprint && window.Fingerprint.headless)", - timeout=60000, - ) - return page.evaluate(_EV) - - -@pytest.mark.e2e -def test_botd_no_detector_flags_automation(firefox_binary, detector_site): - """The real BotD must not flag the build — aggregate AND every one of its - individual detectors (webDriver/userAgent/appVersion/plugins/process/...).""" - botd, _fp, _fps, err = _run_detectors(firefox_binary, detector_site.url) - assert botd is not None, f"BotD produced no result (err:{err!r})" - assert botd.get("bot") is False, ( - f"BotD aggregate flagged a bot: botKind={botd.get('botKind')!r}" - ) - detections = botd.get("detections") or {} - assert detections, f"BotD getDetections() returned nothing (err:{err!r})" - flagged = {k: v.get("botKind") for k, v in detections.items() if v.get("bot")} - assert not flagged, f"BotD individual detectors flagged automation: {flagged}" - - -@pytest.mark.e2e -def test_fingerprintjs_visitorid_stable_across_launches(firefox_binary, detector_site): - """FingerprintJS visitorId must be present and identical across two fresh - launches with the same seed — a real browser is stable; an over-randomized - spoof drifts (and a drifting fingerprint is itself a bot tell).""" - _b1, fp1, _f1, err1 = _run_detectors(firefox_binary, detector_site.url) - _b2, fp2, _f2, err2 = _run_detectors(firefox_binary, detector_site.url) - assert fp1 and fp1.get("visitorId"), f"no visitorId on run 1 (err:{err1!r})" - assert fp2 and fp2.get("visitorId"), f"no visitorId on run 2 (err:{err2!r})" - assert fp1["visitorId"] == fp2["visitorId"], ( - f"FingerprintJS visitorId drifted across launches: " - f"{fp1['visitorId']!r} != {fp2['visitorId']!r} (per-session entropy = bot tell)" - ) - - -@pytest.mark.e2e -def test_fingerprintjs_collects_rich_fingerprint(firefox_binary, detector_site): - """FingerprintJS must collect a RICH component surface (a real browser - exposes many signals; a stripped/blocked surface is itself suspicious).""" - _b, fp, _f, err = _run_detectors(firefox_binary, detector_site.url) - assert fp and fp.get("visitorId"), f"FingerprintJS produced no id (err:{err!r})" - keys = fp.get("componentKeys") or [] - assert len(keys) >= 15, ( - f"FingerprintJS collected only {len(keys)} components — surface too thin " - f"(suppressed signals are themselves a tell): {keys}" - ) - - -@pytest.mark.e2e -def test_fpscanner_no_automation_rules(firefox_binary, detector_site): - """fpscanner's engine-agnostic bot rules (webdriver/selenium/bot-UA/platform/ - timezone/language) must all be clean. The Chrome/GPU-only rules are ignored - on purpose (see module docstring) — they false-red on a software-WebGL host.""" - _b, _fp, fps, err = _run_detectors(firefox_binary, detector_site.url) - assert fps is not None, f"fpscanner produced no result (err:{err!r})" - details = fps.get("details") or {} - assert details, f"fpscanner returned no detection details (err:{err!r})" - flagged = [ - k for k in _FPSCANNER_AGNOSTIC - if details.get(k) and details[k].get("detected") - ] - assert not flagged, ( - f"fpscanner flagged automation on engine-agnostic rules: {flagged} " - f"(full details: { {k: v for k, v in details.items() if v.get('detected')} })" - ) - - -@pytest.mark.e2e -def test_creepjs_headless_and_proxy_clean(firefox_binary, detector_site): - """CreepJS (Firefox-aware) must see no headless tell and no JS-proxy stealth - tell. ``headlessRating`` aggregates webDriverIsOn + headless-UA checks (all - GPU-independent). The proxy/runtime stealth sub-signals (hasIframeProxy, - hasToStringProxy, hasBadChromeRuntime) must be false — a spoof implemented - with a JS Proxy is exactly what CreepJS catches. stealthRating/totalLies/ - likeHeadlessRating are GPU/theme-sensitive, so we log them, not assert.""" - r = _run_creepjs(firefox_binary, detector_site.creep_url) - assert r and r.get("ready"), f"CreepJS never populated window.Fingerprint: {r!r}" - print( - f"[creepjs] headlessRating={r['headlessRating']} stealthRating={r['stealthRating']} " - f"likeHeadlessRating={r['likeHeadlessRating']} totalLies={r['totalLies']} " - f"headless={r['headless']} stealth={r['stealth']}" - ) - assert r["headlessRating"] == 0, ( - f"CreepJS headless tells fired: headless={r['headless']} " - f"(headlessRating={r['headlessRating']})" - ) - stealth = r.get("stealth") or {} - proxy_tells = { - k: stealth.get(k) - for k in ("hasIframeProxy", "hasToStringProxy", "hasBadChromeRuntime") - if stealth.get(k) - } - assert not proxy_tells, f"CreepJS JS-proxy stealth tells fired: {proxy_tells}" diff --git a/tests/test_download.py b/tests/test_download.py index e4159ca..b32dced 100644 --- a/tests/test_download.py +++ b/tests/test_download.py @@ -418,7 +418,7 @@ def test_github_token_none_when_unset(monkeypatch): # Bonus coverage: unsupported platform raises NotImplementedError before any HTTP @pytest.mark.unit def test_ensure_binary_unsupported_platform_raises(monkeypatch): - monkeypatch.setattr("sys.platform", "freebsd") # win32/linux/darwin are supported + monkeypatch.setattr("sys.platform", "darwin") import platform monkeypatch.setattr(platform, "machine", lambda: "AMD64") with pytest.raises(NotImplementedError, match="unsupported platform"): @@ -832,11 +832,3 @@ def test_parse_owner_repo_handles_repos_with_dashes_and_underscores(): ) assert owner == "my-org" assert repo == "my_cool.repo" - - -@pytest.mark.unit -def test_ensure_binary_refuses_known_broken_version(): - """A known-broken release (firefox-8, no juggler) must be refused with a - clear error BEFORE any download — never silently handed to the user.""" - with pytest.raises(RuntimeError, match="known-broken"): - ensure_binary("firefox-8") diff --git a/tests/test_e2e.py b/tests/test_e2e.py index d2e59f2..35fad98 100644 --- a/tests/test_e2e.py +++ b/tests/test_e2e.py @@ -8,9 +8,33 @@ handling) do not need a binary and always run. """ from __future__ import annotations +import sys + import pytest from invisible_playwright import InvisiblePlaywright +from invisible_playwright.constants import BINARY_ENTRY_REL + + +@pytest.fixture(scope="session") +def firefox_binary(): + """Locate the patched Firefox binary or skip the calling test. + + We do NOT trigger a network download here: ``ensure_binary`` would + pull a multi-hundred-megabyte archive from a private release, + which is not appropriate inside a unit/E2E test run. Instead we + look for an already-cached binary; if missing we skip. + """ + if sys.platform not in BINARY_ENTRY_REL: + pytest.skip(f"unsupported platform: {sys.platform}") + from invisible_playwright.download import cache_dir_for_version + entry = cache_dir_for_version() / BINARY_ENTRY_REL[sys.platform] + if not entry.exists(): + pytest.skip( + "patched Firefox binary not cached; run `invisible-playwright fetch` " + "to enable E2E tests" + ) + return str(entry) # ──────────────────────────────────────────────────────────────────── diff --git a/tests/test_fingerprint_consistency.py b/tests/test_fingerprint_consistency.py deleted file mode 100644 index 9912299..0000000 --- a/tests/test_fingerprint_consistency.py +++ /dev/null @@ -1,510 +0,0 @@ -"""Fingerprint consistency / lie-detection tests. - -Complementary to test_fingerprint_surface.py: those tests ask "do you -look like a real browser?" — these ask "are your fingerprint surfaces -INTERNALLY CONSISTENT?" - -Anti-bot systems catch spoofers not by checking each signal in -isolation but by cross-checking related signals. If you spoof UA to -"Windows" but leave navigator.platform as "Linux x86_64", or you spoof -WebGL renderer in the main thread but not in a Web Worker, the -inconsistency proves the spoof is fake. - -Sources studied (all FOSS, MIT-licensed): - - creepjs/src/lies/index.ts — the canonical lie detector - - creepjs/src/worker/index.ts — main-vs-worker scope cross-check - - creepjs/src/math/index.ts — Math.x(p) deterministic equality - - creepjs/src/navigator/index.ts — UA/platform/oscpu invariants - - niespodd/browser-fingerprinting README — worker hwConcurrency, - plugin chain, perf.timeOrigin - -Everything runs against `about:blank` with NO network and NO proxy. - -Run only this file: - pytest tests/test_fingerprint_consistency.py -m e2e -v -""" -from __future__ import annotations - -import pytest - -from invisible_playwright import InvisiblePlaywright - - -PIN = { - "screen.width": 1920, - "screen.height": 1080, - "screen.avail_width": 1920, - "screen.avail_height": 1040, - "screen.dpr": 1.0, - "hardware.concurrency": 8, - "audio.sample_rate": 48000, - "audio.max_channel_count": 2, -} - - -@pytest.fixture(scope="module") -def page(firefox_binary): - with InvisiblePlaywright( - seed=42, - pin=PIN, - binary_path=firefox_binary, - headless=True, - ) as browser: - ctx = browser.new_context() - p = ctx.new_page() - p.goto("about:blank", timeout=30_000) - yield p - - -def _ev(page, expr): - return page.evaluate(expr) - - -# =========================================================================== -# 1. Math determinism — same input MUST yield same output -# Source: creepjs/src/math/index.ts -# A wrapper that adds noise to Math.* (canvas-spoofing prefs) exposes -# itself here: two consecutive calls with the same input must be -# byte-identical. -# =========================================================================== - - -@pytest.mark.e2e -@pytest.mark.parametrize("fn,arg", [ - ("cos", "1e308"), - ("acos", "0.5"), - ("asin", "0.5"), - ("atan", "Math.PI"), - ("atanh", "0.5"), - ("cbrt", "Math.PI"), - ("cosh", "Math.PI"), - ("exp", "Math.PI"), - ("expm1", "Math.PI"), - ("log", "Math.PI"), - ("log1p", "Math.PI"), - ("log10", "Math.PI"), - ("sin", "Math.PI"), - ("sinh", "Math.PI"), - ("sqrt", "Math.PI"), - ("tan", "Math.PI"), - ("tanh", "Math.PI"), -]) -def test_math_determinism(page, fn, arg): - """Math.() must return the same value across 100 calls.""" - first, last, all_equal = _ev(page, f"""() => {{ - const r = []; - for (let i = 0; i < 100; i++) r.push(Math.{fn}({arg})); - return [r[0], r[99], r.every(x => Object.is(x, r[0]))]; - }}""") - assert all_equal, ( - f"Math.{fn}({arg}) drifts across calls: first={first}, last={last}" - ) - - -@pytest.mark.e2e -def test_math_pow_two_arg_determinism(page): - ok = _ev(page, """() => { - const a = Math.pow(Math.PI, 2); - for (let i = 0; i < 50; i++) { - if (!Object.is(Math.pow(Math.PI, 2), a)) return false; - } - return true; - }""") - assert ok - - -# =========================================================================== -# 2. Worker scope vs main thread — navigator properties MUST agree -# Source: creepjs/src/worker/index.ts -# =========================================================================== - - -def _worker_navigator_dict(page, props): - expr = """async (props) => { - const code = ` - self.onmessage = (e) => { - const out = {}; - for (const p of e.data) { - try { out[p] = self.navigator[p]; } - catch (err) { out[p] = ''; } - } - if (out.languages && Array.isArray(out.languages)) { - out.languages = [...out.languages]; - } - self.postMessage(out); - }; - `; - const blob = new Blob([code], { type: 'application/javascript' }); - const url = URL.createObjectURL(blob); - const worker = new Worker(url); - try { - const result = await new Promise((resolve, reject) => { - worker.onmessage = (e) => resolve(e.data); - worker.onerror = (e) => reject(new Error(e.message)); - worker.postMessage(props); - setTimeout(() => reject(new Error('worker timeout')), 5000); - }); - return result; - } finally { - worker.terminate(); - URL.revokeObjectURL(url); - } - }""" - return page.evaluate(expr, list(props)) - - -@pytest.mark.e2e -def test_worker_userAgent_matches_main(page): - main = _ev(page, "navigator.userAgent") - worker = _worker_navigator_dict(page, ("userAgent",)) - assert worker["userAgent"] == main, ( - f"UA drift main vs worker:\n main: {main!r}\n worker: {worker['userAgent']!r}" - ) - - -@pytest.mark.e2e -def test_worker_hardwareConcurrency_matches_main(page): - main = _ev(page, "navigator.hardwareConcurrency") - worker = _worker_navigator_dict(page, ("hardwareConcurrency",)) - assert worker["hardwareConcurrency"] == main - - -@pytest.mark.e2e -def test_worker_language_matches_main(page): - main = _ev(page, "navigator.language") - worker = _worker_navigator_dict(page, ("language",)) - assert worker["language"] == main - - -@pytest.mark.e2e -def test_worker_languages_matches_main(page): - main = _ev(page, "[...navigator.languages]") - worker = _worker_navigator_dict(page, ("languages",)) - assert list(worker["languages"]) == list(main) - - -@pytest.mark.e2e -def test_worker_platform_matches_main(page): - main = _ev(page, "navigator.platform") - worker = _worker_navigator_dict(page, ("platform",)) - assert worker["platform"] == main - - -# =========================================================================== -# 3. Iframe scope vs window scope -# Source: creepjs/src/lies/index.ts (getBehemothIframe pattern) -# =========================================================================== - - -def _iframe_navigator_dict(page, props): - expr = """(props) => { - const iframe = document.createElement('iframe'); - iframe.style.display = 'none'; - document.body.appendChild(iframe); - const out = {}; - for (const p of props) { - try { out[p] = iframe.contentWindow.navigator[p]; } - catch (e) { out[p] = ''; } - } - if (Array.isArray(out.languages)) out.languages = [...out.languages]; - document.body.removeChild(iframe); - return out; - }""" - return page.evaluate(expr, list(props)) - - -@pytest.mark.e2e -def test_iframe_userAgent_matches_window(page): - main = _ev(page, "navigator.userAgent") - iframe = _iframe_navigator_dict(page, ("userAgent",)) - assert iframe["userAgent"] == main - - -@pytest.mark.e2e -def test_iframe_language_matches_window(page): - main = _ev(page, "navigator.language") - iframe = _iframe_navigator_dict(page, ("language",)) - assert iframe["language"] == main - - -@pytest.mark.e2e -def test_iframe_hardwareConcurrency_matches_window(page): - main = _ev(page, "navigator.hardwareConcurrency") - iframe = _iframe_navigator_dict(page, ("hardwareConcurrency",)) - assert iframe["hardwareConcurrency"] == main - - -@pytest.mark.e2e -def test_iframe_screen_matches_window(page): - main = _ev(page, "[screen.width, screen.height]") - iframe = _ev(page, """() => { - const f = document.createElement('iframe'); - f.style.display = 'none'; - document.body.appendChild(f); - const v = [f.contentWindow.screen.width, f.contentWindow.screen.height]; - document.body.removeChild(f); - return v; - }""") - assert iframe == main - - -# =========================================================================== -# 4. UA self-consistency (creepjs/src/navigator/index.ts) -# =========================================================================== - - -@pytest.mark.e2e -def test_navigator_platform_matches_userAgent_OS(page): - ua = _ev(page, "navigator.userAgent") - platform = _ev(page, "navigator.platform") - if "Windows" in ua: - assert "Win" in platform - elif "Mac" in ua: - assert "Mac" in platform - elif "Linux" in ua or "X11" in ua: - assert "Linux" in platform or "X11" in platform - - -@pytest.mark.e2e -def test_navigator_oscpu_matches_userAgent(page): - """Firefox-only: navigator.oscpu must correlate with UA OS.""" - ua = _ev(page, "navigator.userAgent") - oscpu = _ev(page, "navigator.oscpu || ''") - if not oscpu: - pytest.skip("navigator.oscpu not exposed") - if "Windows" in ua: - assert "Windows" in oscpu - elif "Linux" in ua: - assert "Linux" in oscpu - elif "Mac" in ua: - assert "Mac" in oscpu - - -# =========================================================================== -# 5. Native function self-toString (creepjs/src/lies/index.ts hasKnownToString) -# =========================================================================== - - -def _is_native_toString(text, fn_name): - """Mirror of CreepJS hasKnownToString — accept the engine-specific - native patterns (single-line on V8, multi-line on SpiderMonkey).""" - import re as _re - name = _re.escape(fn_name) - patterns = [ - rf"^function {name}\(\) \{{ \[native code\] \}}$", - rf"^function get {name}\(\) \{{ \[native code\] \}}$", - rf"^function {name}\(\) \{{[\s\S]*\[native code\][\s\S]*\}}$", - rf"^function get {name}\(\) \{{[\s\S]*\[native code\][\s\S]*\}}$", - ] - return any(_re.match(p, text) for p in patterns) - - -@pytest.mark.e2e -@pytest.mark.parametrize("native_fn,name", [ - ("Function.prototype.toString", "toString"), - ("Function.prototype.bind", "bind"), - ("Function.prototype.call", "call"), - ("Function.prototype.apply", "apply"), - ("Object.getOwnPropertyDescriptor", "getOwnPropertyDescriptor"), - ("Object.defineProperty", "defineProperty"), - ("Array.prototype.slice", "slice"), - ("JSON.stringify", "stringify"), -]) -def test_native_function_self_toString_matches(page, native_fn, name): - """Each native function's `.toString()` must match its engine's - native pattern. A Proxy wrapper or function-rewrite leaks here.""" - text = _ev(page, f"{native_fn}.toString()") - assert _is_native_toString(text, name), ( - f"{native_fn}.toString() not native-shape: {text!r}" - ) - - -# =========================================================================== -# 6. AudioContext / WebGL determinism -# =========================================================================== - - -@pytest.mark.e2e -def test_audio_offline_context_deterministic(page): - """OfflineAudioContext: same graph → byte-identical output.""" - ok = _ev(page, """async () => { - async function render() { - const ctx = new (window.OfflineAudioContext || - window.webkitOfflineAudioContext)(1, 5000, 44100); - const osc = ctx.createOscillator(); - osc.connect(ctx.destination); - osc.start(0); - const buf = await ctx.startRendering(); - return Array.from(buf.getChannelData(0).slice(0, 50)); - } - const a = await render(); - const b = await render(); - return JSON.stringify(a) === JSON.stringify(b); - }""") - assert ok - - -@pytest.mark.e2e -def test_webgl_getParameter_deterministic(page): - """WebGL parameters must not drift across reads.""" - ok = _ev(page, """() => { - const c = document.createElement('canvas'); - const gl = c.getContext('webgl'); - if (!gl) return false; - const params = [gl.MAX_TEXTURE_SIZE, gl.MAX_VIEWPORT_DIMS, - gl.MAX_RENDERBUFFER_SIZE, gl.MAX_VERTEX_ATTRIBS]; - const ref = JSON.stringify(params.map(p => gl.getParameter(p))); - for (let i = 0; i < 50; i++) { - if (JSON.stringify(params.map(p => gl.getParameter(p))) !== ref) { - return false; - } - } - return true; - }""") - assert ok - - -# =========================================================================== -# 7. Locale ↔ Intl cross-consistency -# =========================================================================== - - -@pytest.mark.e2e -def test_navigator_language_matches_Intl_locale(page): - """navigator.language base must agree with Intl.DateTimeFormat locale.""" - nav = _ev(page, "navigator.language").split("-")[0] - intl = _ev(page, - "Intl.DateTimeFormat().resolvedOptions().locale").split("-")[0] - assert nav == intl, ( - f"navigator.language base={nav!r} vs Intl={intl!r}" - ) - - -@pytest.mark.e2e -def test_navigator_language_matches_Intl_NumberFormat(page): - nav = _ev(page, "navigator.language").split("-")[0] - num = _ev(page, - "Intl.NumberFormat().resolvedOptions().locale").split("-")[0] - assert nav == num - - -@pytest.mark.e2e -def test_navigator_language_matches_Intl_Collator(page): - nav = _ev(page, "navigator.language").split("-")[0] - col = _ev(page, - "(new Intl.Collator()).resolvedOptions().locale").split("-")[0] - assert nav == col - - -# =========================================================================== -# 8. Property descriptor shape lies -# Spoofers using Object.defineProperty(navigator, prop, {value: ...}) -# leave a 'value' field on the descriptor — real native props use a getter. -# =========================================================================== - - -_DESCRIPTOR_NATIVE_PROPS = [ - "userAgent", "platform", "hardwareConcurrency", "language", "languages", - "vendor", "appVersion", "appName", "appCodeName", "doNotTrack", - "cookieEnabled", "onLine", "product", "productSub", "buildID", "oscpu", -] - - -@pytest.mark.e2e -@pytest.mark.parametrize("prop", _DESCRIPTOR_NATIVE_PROPS) -def test_navigator_property_descriptor_is_getter_not_value(page, prop): - """Each spoofable navigator.* property must be defined via a native - getter — NOT Object.defineProperty(..., {value: x}). The value-field - descriptor is the lazy spoof leak CreepJS catches.""" - has_lie = _ev(page, f"""() => {{ - let proto = navigator; - let descriptor = null; - while (proto && !descriptor) {{ - descriptor = Object.getOwnPropertyDescriptor(proto, {prop!r}); - proto = Object.getPrototypeOf(proto); - }} - if (!descriptor) return null; - return 'value' in descriptor; - }}""") - if has_lie is None: - pytest.skip(f"navigator.{prop} not exposed") - assert has_lie is False, ( - f"navigator.{prop} descriptor exposes 'value' field — lazy spoof" - ) - - -# =========================================================================== -# 9. performance.timeOrigin + monotonic -# =========================================================================== - - -@pytest.mark.e2e -def test_performance_timeOrigin_stable(page): - assert _ev(page, - "performance.timeOrigin === performance.timeOrigin") - - -@pytest.mark.e2e -def test_performance_now_monotonic(page): - ok = _ev(page, """() => { - let prev = performance.now(); - for (let i = 0; i < 100; i++) { - const cur = performance.now(); - if (cur < prev) return false; - prev = cur; - } - return true; - }""") - assert ok - - -# =========================================================================== -# 10. Window dimension invariants -# =========================================================================== - - -@pytest.mark.e2e -def test_window_inner_not_larger_than_outer(page): - inner, outer = _ev(page, "[window.innerWidth, window.outerWidth]") - assert inner <= outer - - -@pytest.mark.e2e -def test_screen_avail_not_larger_than_screen(page): - aw, w = _ev(page, "[screen.availWidth, screen.width]") - ah, h = _ev(page, "[screen.availHeight, screen.height]") - assert aw <= w and ah <= h - - -# =========================================================================== -# 11. Firefox UA invariants -# =========================================================================== - - -@pytest.mark.e2e -def test_firefox_UA_implies_empty_vendor(page): - """Firefox: navigator.vendor === ''""" - if "Firefox" not in _ev(page, "navigator.userAgent"): - pytest.skip("Firefox-only invariant") - if "Chrome" in _ev(page, "navigator.userAgent"): - pytest.skip("Chrome+Firefox UA — likely synthetic") - assert _ev(page, "navigator.vendor") == "" - - -@pytest.mark.e2e -def test_firefox_appVersion_short_form(page): - """Real Firefox's appVersion is '5.0 (Windows)' form, not the full UA.""" - if "Firefox" not in _ev(page, "navigator.userAgent"): - pytest.skip("Firefox-only invariant") - av = _ev(page, "navigator.appVersion") - ua = _ev(page, "navigator.userAgent") - assert av.startswith("5.0 (") - assert len(av) < len(ua) - - -@pytest.mark.e2e -def test_firefox_UA_implies_appName_Netscape(page): - """navigator.appName === 'Netscape' (historical invariant).""" - if "Firefox" not in _ev(page, "navigator.userAgent"): - pytest.skip("Firefox-only invariant") - assert _ev(page, "navigator.appName") == "Netscape" diff --git a/tests/test_fingerprint_surface.py b/tests/test_fingerprint_surface.py deleted file mode 100644 index 8a9ff68..0000000 --- a/tests/test_fingerprint_surface.py +++ /dev/null @@ -1,311 +0,0 @@ -"""Fingerprint surface tests — replicate the checks performed by the canonical -anti-bot detection libraries against an OFFLINE browser session. - -Each test asserts the SAME thing the upstream detector would flag. A pass -here means our patched build appears human to that detector; a fail -means a real stealth hole that anti-bot kits would exploit in production. - -Detector libraries studied (all FOSS, MIT-licensed): - - github.com/fingerprintjs/BotD — 19 detectors, the most - widely deployed client-side - bot detector - - github.com/abrahamjuliot/creepjs — headless / stealth / lies - modules - - github.com/fingerprintjs/fingerprintjs — canvas / audio / color / - touch consistency - - github.com/antoinevastel/fpscanner — UA / platform / oscpu - cross-checks - - bot.sannysoft.com — classic Puppeteer harness - -Everything runs against `about:blank` with NO network and NO proxy. The -suite is intended to be part of the release-gate: pre-push hook runs -`pytest -m e2e` and these tests must be green on every release. - -Run only this file: - pytest tests/test_fingerprint_surface.py -m e2e -v -""" -from __future__ import annotations - -import re -import sys - -import pytest - -from invisible_playwright import InvisiblePlaywright - - -# ──────────────────────────────────────────────────────────────────── -# Inline PIN — a coherent mid-range Windows desktop. Not user-config: -# these specific values are what the surface tests assert against. -# Keep PIN small (only fields that JS exposes) and stable across runs. -# ──────────────────────────────────────────────────────────────────── - -PIN = { - "screen.width": 1920, - "screen.height": 1080, - "screen.avail_width": 1920, - "screen.avail_height": 1040, - "screen.dpr": 1.0, - "hardware.concurrency": 8, - "audio.sample_rate": 48000, - "audio.max_channel_count": 2, -} - - -@pytest.fixture(scope="module") -def page(firefox_binary): - """One headless browser shared across the whole module. - ~20s startup paid once, then every test runs in ~50ms.""" - with InvisiblePlaywright( - seed=42, - pin=PIN, - binary_path=firefox_binary, - headless=True, - ) as browser: - ctx = browser.new_context() - p = ctx.new_page() - p.goto("about:blank", timeout=30_000) - yield p - - -def _ev(page, expr): - return page.evaluate(expr) - - -# =========================================================================== -# sannysoft.com — classic Puppeteer detection harness -# =========================================================================== - - -@pytest.mark.e2e -def test_sannysoft_chrome_object_consistency(page): - """Firefox UA + window.chrome present = bot-framework leak.""" - if "Firefox" in _ev(page, "navigator.userAgent"): - assert not _ev(page, "typeof window.chrome !== 'undefined'") - - -@pytest.mark.e2e -def test_sannysoft_permissions_query_works(page): - """navigator.permissions.query() must return a proper PermissionStatus.""" - ok = _ev(page, """async () => { - if (!navigator.permissions || !navigator.permissions.query) return false; - try { - const r = await navigator.permissions.query({name: 'notifications'}); - return r && typeof r.state === 'string'; - } catch (e) { return false; } - }""") - assert ok - - -@pytest.mark.e2e -def test_sannysoft_iframe_chrome_not_leaked(page): - """iframe.contentWindow.chrome must not leak on Firefox UA.""" - if "Firefox" not in _ev(page, "navigator.userAgent"): - pytest.skip("Firefox-only invariant") - leaks = _ev(page, """() => { - const iframe = document.createElement('iframe'); - iframe.style.display = 'none'; - document.body.appendChild(iframe); - const is = typeof iframe.contentWindow.chrome !== 'undefined'; - document.body.removeChild(iframe); - return is; - }""") - assert not leaks - - -@pytest.mark.e2e -def test_sannysoft_iframe_languages_not_empty(page): - """Iframe-scope navigator.languages must have ≥1 entry.""" - n = _ev(page, """() => { - const f = document.createElement('iframe'); - f.style.display = 'none'; - document.body.appendChild(f); - const len = f.contentWindow.navigator.languages.length; - document.body.removeChild(f); - return len; - }""") - assert n > 0 - - -# =========================================================================== -# FingerprintJS — fingerprint surface coherence -# =========================================================================== - - -@pytest.mark.e2e -def test_fpjs_canvas_2d_context_returns_valid(page): - ok = _ev(page, """() => { - const c = document.createElement('canvas'); - c.width = 100; c.height = 100; - const ctx = c.getContext('2d'); - if (!ctx) return false; - ctx.fillText('test', 10, 10); - const data = c.toDataURL(); - return data.length > 100 && data.startsWith('data:image/png;base64'); - }""") - assert ok - - -@pytest.mark.e2e -def test_fpjs_audio_context_works(page): - ok = _ev(page, """async () => { - try { - const ctx = new (window.OfflineAudioContext || - window.webkitOfflineAudioContext)(1, 5000, 44100); - const osc = ctx.createOscillator(); - osc.connect(ctx.destination); - osc.start(0); - const buf = await ctx.startRendering(); - return buf && buf.length > 0; - } catch (e) { return false; } - }""") - assert ok - - -@pytest.mark.e2e -def test_fpjs_color_gamut_query_works(page): - """matchMedia('(color-gamut: ...)') must match at least srgb.""" - ok = _ev(page, """matchMedia('(color-gamut: srgb)').matches || - matchMedia('(color-gamut: p3)').matches || - matchMedia('(color-gamut: rec2020)').matches""") - assert ok - - -@pytest.mark.e2e -def test_fpjs_screen_color_depth_realistic(page): - """Atypical color depths are headless-distinctive.""" - cd = _ev(page, "screen.colorDepth") - assert cd in (24, 30, 32) - - -# =========================================================================== -# PIN-locked surfaces (the values declared in PIN above) -# =========================================================================== - - -@pytest.mark.e2e -def test_pin_screen_width_lands_in_screen_object(page): - assert _ev(page, "screen.width") == PIN["screen.width"] - - -@pytest.mark.e2e -def test_pin_screen_height_lands_in_screen_object(page): - assert _ev(page, "screen.height") == PIN["screen.height"] - - -@pytest.mark.e2e -def test_pin_hardware_concurrency_lands_in_navigator(page): - assert (_ev(page, "navigator.hardwareConcurrency") - == PIN["hardware.concurrency"]) - - -@pytest.mark.e2e -def test_pin_audio_sample_rate_lands_in_AudioContext(page): - assert _ev(page, - "(new (window.AudioContext||window.webkitAudioContext)()).sampleRate" - ) == PIN["audio.sample_rate"] - - -@pytest.mark.e2e -def test_pin_audio_max_channels_lands_in_destination(page): - assert _ev(page, - "(new (window.AudioContext||window.webkitAudioContext)())" - ".destination.maxChannelCount" - ) == PIN["audio.max_channel_count"] - - -# =========================================================================== -# fpscanner-style cross-checks -# =========================================================================== - - -@pytest.mark.e2e -def test_fpscanner_ua_vs_platform_consistent(page): - """UA OS substring must agree with navigator.platform OS substring.""" - ua = _ev(page, "navigator.userAgent") - platform = _ev(page, "navigator.platform") - if "Windows" in ua: - assert "Win" in platform, f"UA Win but platform={platform!r}" - elif "Mac" in ua: - assert "Mac" in platform - elif "Linux" in ua: - assert "Linux" in platform or "X11" in platform - - -@pytest.mark.e2e -def test_fpscanner_no_userAgentData_on_firefox(page): - """navigator.userAgentData is Chromium-only. Presence on Firefox UA = bot.""" - if "Firefox" in _ev(page, "navigator.userAgent"): - assert not _ev(page, "'userAgentData' in navigator") - - -# =========================================================================== -# WebGL masking-detector guard (pixelscan getFixedRedBox / webglHash) -# -# pixelscan flags "fingerprint masking" on the WebGL readPixels output. We -# reproduce ITS probe locally (the fingerprintjs gradient triangle) and check -# the structural signature it keys on: our stealth readPixels noise MUST be a -# coherent, monotonic gamma remap (smooth, ~0 spikes), NOT isolated +-1 flips -# (which read as unnatural high-frequency noise and were flagged as masking). -# This is the CI-safe local stand-in for pixelscan's server-side check; it -# guards the gamma fix from ever silently regressing to the +-1 algorithm. -# =========================================================================== - -_WEBGL_MASKING_PROBE = """() => { - const c = document.createElement('canvas'); - const gl = c.getContext('webgl') || c.getContext('experimental-webgl'); - if (!gl) return { error: 'no-webgl' }; - const vs = 'attribute vec2 a;uniform vec2 o;varying vec2 v;' + - 'void main(){v=a+o;gl_Position=vec4(a,0,1);}'; - const fs = 'precision mediump float;varying vec2 v;' + - 'void main(){gl_FragColor=vec4(v,0,1);}'; - const buf = gl.createBuffer(); gl.bindBuffer(gl.ARRAY_BUFFER, buf); - gl.bufferData(gl.ARRAY_BUFFER, - new Float32Array([-0.2,-0.9,0, 0.4,-0.26,0, 0,0.732134444,0]), gl.STATIC_DRAW); - const p = gl.createProgram(); - const s1 = gl.createShader(gl.VERTEX_SHADER); gl.shaderSource(s1, vs); gl.compileShader(s1); - const s2 = gl.createShader(gl.FRAGMENT_SHADER); gl.shaderSource(s2, fs); gl.compileShader(s2); - gl.attachShader(p, s1); gl.attachShader(p, s2); gl.linkProgram(p); gl.useProgram(p); - const loc = gl.getAttribLocation(p, 'a'); gl.enableVertexAttribArray(loc); - gl.vertexAttribPointer(loc, 3, gl.FLOAT, false, 0, 0); - const off = gl.getUniformLocation(p, 'o'); gl.uniform2f(off, 1, 1); - gl.drawArrays(gl.TRIANGLE_STRIP, 0, 3); - const w = gl.drawingBufferWidth, h = gl.drawingBufferHeight; - const px = new Uint8Array(w * h * 4); - gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px); - // count small local extrema (|delta|<=3 to both horizontal neighbours, same - // sign) — the +-1-noise signature; a smooth/monotonic render has ~none. - let spikes = 0; - for (let y = 0; y < h; y++) { - for (let x = 1; x < w - 1; x++) { - for (let ch = 0; ch < 3; ch++) { - const i = (y * w + x) * 4 + ch; const val = px[i]; - if (val === 0) continue; - const dl = val - px[i - 4], dr = val - px[i + 4]; - if (dl * dr > 0 && Math.abs(dl) <= 3 && Math.abs(dr) <= 3) spikes++; - } - } - } - return { spikes: spikes, dims: w + 'x' + h }; -}""" - - -@pytest.mark.e2e -def test_webgl_readpixels_no_masking_signature(page): - """Stealth WebGL readPixels noise must be a coherent gamma remap (smooth), - not isolated +-1 flips. +-1 noise on the smooth gradient triangle produced - ~300+ 'spikes' and pixelscan flagged it as masking; the gamma remap leaves - the gradient smooth (~0 spikes). Regression guard for the gamma fix.""" - res = _ev(page, _WEBGL_MASKING_PROBE) - if res.get("error") == "no-webgl" and sys.platform == "darwin": - pytest.skip( - "macOS CI runners expose no WebGL (no software-GL fallback); the gamma " - "readPixels remap is platform-agnostic C++ and is exercised by the Linux " - "(Xvfb/llvmpipe) and Windows (WARP) gates." - ) - assert "error" not in res, f"WebGL probe failed: {res}" - # genuine / gamma -> ~0; the rejected +-1 algorithm produced ~320. - assert res["spikes"] < 30, ( - f"WebGL readPixels shows {res['spikes']} high-frequency noise spikes " - f"(pixelscan-maskable); the stealth noise must be a smooth gamma remap." - ) diff --git a/tests/test_geo.py b/tests/test_geo.py deleted file mode 100644 index f56322f..0000000 --- a/tests/test_geo.py +++ /dev/null @@ -1,325 +0,0 @@ -"""Unit tests for `invisible_playwright._geo` (timezone="auto" resolution). - -Covers: the precedence policy (resolve_session_timezone), proxy→requests -translation, egress IP discovery (mocked HTTP), and IP→IANA mapping (mocked -mmdb). No real network or mmdb is touched. -""" -import sys -import types - -import pytest - -from invisible_playwright import _geo -from invisible_playwright._geo import ( - GeoTimezoneError, - _proxies_for_requests, - _proxy_is_set, - discover_egress_ip, - ip_to_timezone, - prepare_session_geo, - resolve_session_timezone, -) - -SOCKS = {"server": "socks5://gw.example:1080", "username": "u", "password": "p"} -HTTP = {"server": "http://gw.example:8080", "username": "u", "password": "p"} - - -# ────────────────────────────────────────────────────────────────────── -# _proxy_is_set -# ────────────────────────────────────────────────────────────────────── -@pytest.mark.unit -@pytest.mark.parametrize( - "proxy,expected", - [ - (None, False), - ({}, False), - ({"server": ""}, False), - ({"server": " "}, False), - ({"server": "direct://"}, False), - ({"server": "DIRECT://"}, False), - ({"server": "socks5://h:1"}, True), - ({"server": "http://h:8080"}, True), - ], -) -def test_proxy_is_set(proxy, expected): - assert _proxy_is_set(proxy) is expected - - -# ────────────────────────────────────────────────────────────────────── -# _proxies_for_requests — scheme + credential translation -# ────────────────────────────────────────────────────────────────────── -@pytest.mark.unit -def test_proxies_socks5_uses_socks5h_remote_dns(): - out = _proxies_for_requests(SOCKS) - assert out["http"] == "socks5h://u:p@gw.example:1080" - assert out["https"] == out["http"] - - -@pytest.mark.unit -def test_proxies_socks4_scheme(): - out = _proxies_for_requests({"server": "socks4://gw:1080"}) - assert out["http"] == "socks4://gw:1080" - - -@pytest.mark.unit -def test_proxies_http_and_https_schemes(): - assert _proxies_for_requests(HTTP)["http"] == "http://u:p@gw.example:8080" - out = _proxies_for_requests({"server": "https://gw:8443"}) - assert out["https"] == "https://gw:8443" - - -@pytest.mark.unit -def test_proxies_no_scheme_defaults_to_http(): - out = _proxies_for_requests({"server": "gw.example:3128"}) - assert out["http"] == "http://gw.example:3128" - - -@pytest.mark.unit -def test_proxies_credentials_are_url_encoded(): - out = _proxies_for_requests( - {"server": "socks5://gw:1080", "username": "user@x", "password": "p:w/d"} - ) - # '@', ':' and '/' in creds must be percent-encoded so they don't break - # the proxy URL parsing. - assert "user%40x:p%3Aw%2Fd@gw:1080" in out["http"] - - -@pytest.mark.unit -def test_proxies_no_credentials_has_no_auth_prefix(): - out = _proxies_for_requests({"server": "socks5://gw:1080"}) - assert out["http"] == "socks5h://gw:1080" - - -# ────────────────────────────────────────────────────────────────────── -# discover_egress_ip — mocked requests -# ────────────────────────────────────────────────────────────────────── -class _FakeResp: - def __init__(self, text, status=200): - self.text = text - self._status = status - - def raise_for_status(self): - if self._status >= 400: - raise RuntimeError(f"HTTP {self._status}") - - -@pytest.mark.unit -def test_discover_egress_ip_first_endpoint_wins(monkeypatch): - calls = [] - - def fake_get(url, **kw): - calls.append(url) - return _FakeResp("203.0.113.7\n") - - monkeypatch.setattr(_geo.requests, "get", fake_get) - assert discover_egress_ip(SOCKS) == "203.0.113.7" - assert len(calls) == 1 # stopped at the first success - - -@pytest.mark.unit -def test_discover_egress_ip_falls_through_to_next_on_error(monkeypatch): - seq = iter([_FakeResp("junk-not-an-ip"), _FakeResp("198.51.100.42")]) - - def fake_get(url, **kw): - return next(seq) - - monkeypatch.setattr(_geo.requests, "get", fake_get) - assert discover_egress_ip(HTTP) == "198.51.100.42" - - -@pytest.mark.unit -def test_discover_egress_ip_all_fail_raises(monkeypatch): - def fake_get(url, **kw): - raise OSError("connection refused") - - monkeypatch.setattr(_geo.requests, "get", fake_get) - with pytest.raises(GeoTimezoneError): - discover_egress_ip(SOCKS) - - -@pytest.mark.unit -def test_discover_egress_ip_no_proxy_is_direct(monkeypatch): - # proxy=None → direct request, requests.get must get proxies=None. - seen = {} - - def fake_get(url, **kw): - seen["proxies"] = kw.get("proxies", "MISSING") - return _FakeResp("192.0.2.55") - - monkeypatch.setattr(_geo.requests, "get", fake_get) - assert discover_egress_ip(None) == "192.0.2.55" - assert seen["proxies"] is None - - -# ────────────────────────────────────────────────────────────────────── -# ip_to_timezone — mocked mmdb reader -# ────────────────────────────────────────────────────────────────────── -class _FakeReader: - def __init__(self, record): - self._record = record - - def __enter__(self): - return self - - def __exit__(self, *a): - return False - - def get(self, ip): - return self._record - - -def _install_fake_maxminddb(monkeypatch, record): - mod = types.ModuleType("maxminddb") - mod.open_database = lambda path: _FakeReader(record) - monkeypatch.setitem(sys.modules, "maxminddb", mod) - - -@pytest.mark.unit -def test_ip_to_timezone_reads_location_time_zone(monkeypatch): - _install_fake_maxminddb(monkeypatch, {"location": {"time_zone": "Europe/Rome"}}) - assert ip_to_timezone("1.2.3.4", "x.mmdb") == "Europe/Rome" - - -@pytest.mark.unit -def test_ip_to_timezone_ip_absent_raises(monkeypatch): - _install_fake_maxminddb(monkeypatch, None) - with pytest.raises(GeoTimezoneError): - ip_to_timezone("1.2.3.4", "x.mmdb") - - -@pytest.mark.unit -def test_ip_to_timezone_missing_zone_raises(monkeypatch): - _install_fake_maxminddb(monkeypatch, {"location": {}}) - with pytest.raises(GeoTimezoneError): - ip_to_timezone("1.2.3.4", "x.mmdb") - - -@pytest.mark.unit -def test_ip_to_timezone_invalid_iana_raises(monkeypatch): - _install_fake_maxminddb(monkeypatch, {"location": {"time_zone": "Not/AZone"}}) - with pytest.raises(GeoTimezoneError): - ip_to_timezone("1.2.3.4", "x.mmdb") - - -# ────────────────────────────────────────────────────────────────────── -# resolve_session_timezone — the precedence policy -# ────────────────────────────────────────────────────────────────────── -@pytest.fixture -def stub_egress(monkeypatch): - """Make egress resolution deterministic + offline; record if it ran.""" - state = {"called": False} - - def fake_discover(proxy=None, **kw): - state["called"] = True - state["proxy_arg"] = proxy - return "203.0.113.7" - - monkeypatch.setattr(_geo, "discover_egress_ip", fake_discover) - monkeypatch.setattr(_geo, "ip_to_timezone", lambda ip, mmdb: "America/New_York") - # ensure_geoip_mmdb is imported from .download at call time - import invisible_playwright.download as dl - - monkeypatch.setattr(dl, "ensure_geoip_mmdb", lambda *a, **k: "fake.mmdb") - return state - - -@pytest.mark.unit -def test_resolve_explicit_iana_wins(stub_egress): - # An explicit zone wins and never triggers resolution (proxy or not). - assert resolve_session_timezone("Asia/Tokyo", SOCKS) == "Asia/Tokyo" - assert resolve_session_timezone("Asia/Tokyo", None) == "Asia/Tokyo" - assert stub_egress["called"] is False - - -@pytest.mark.unit -def test_resolve_empty_with_proxy_resolves_from_proxy(stub_egress): - assert resolve_session_timezone("", SOCKS) == "America/New_York" - assert stub_egress["called"] is True - assert stub_egress["proxy_arg"] == SOCKS # routed through the proxy - - -@pytest.mark.unit -def test_resolve_auto_with_proxy_resolves_from_proxy(stub_egress): - assert resolve_session_timezone("auto", HTTP) == "America/New_York" - assert stub_egress["proxy_arg"] == HTTP - - -@pytest.mark.unit -def test_resolve_empty_no_proxy_resolves_from_host(stub_egress): - # auto ALWAYS resolves — without a proxy, from the host's own public IP. - assert resolve_session_timezone("", None) == "America/New_York" - assert stub_egress["called"] is True - assert stub_egress["proxy_arg"] is None # direct request, no proxy - - -@pytest.mark.unit -def test_resolve_auto_no_proxy_resolves_from_host(stub_egress): - assert resolve_session_timezone("auto", None) == "America/New_York" - assert stub_egress["proxy_arg"] is None - - -@pytest.mark.unit -def test_resolve_direct_proxy_resolves_via_host(stub_egress): - # direct:// counts as "no proxy" → resolve from the host IP, don't skip. - assert resolve_session_timezone("auto", {"server": "direct://"}) == "America/New_York" - assert stub_egress["proxy_arg"] is None - - -@pytest.mark.unit -def test_resolve_no_proxy_failure_falls_back_to_host(monkeypatch): - # Without a proxy, a lookup failure must NOT break the launch → host TZ (""). - def boom(proxy=None, **kw): - raise GeoTimezoneError("offline") - - monkeypatch.setattr(_geo, "discover_egress_ip", boom) - assert resolve_session_timezone("auto", None) == "" - assert resolve_session_timezone("", None) == "" - - -@pytest.mark.unit -def test_resolve_proxy_failure_raises(monkeypatch): - # With a proxy set, a failure must raise — never a silent host-TZ fallback. - def boom(proxy=None, **kw): - raise GeoTimezoneError("no egress") - - monkeypatch.setattr(_geo, "discover_egress_ip", boom) - with pytest.raises(GeoTimezoneError): - resolve_session_timezone("auto", SOCKS) - with pytest.raises(GeoTimezoneError): - resolve_session_timezone("", SOCKS) - - -# ────────────────────────────────────────────────────────────────────── -# prepare_session_geo — one round-trip for BOTH timezone + the WebRTC -# egress IP. The egress feeds the srflx override (only behind a proxy). -# ────────────────────────────────────────────────────────────────────── -@pytest.mark.unit -def test_prepare_geo_egress_present_behind_proxy(stub_egress): - geo = prepare_session_geo("auto", SOCKS) - assert geo.timezone == "America/New_York" - assert geo.egress_ip == "203.0.113.7" # discovered for WebRTC - assert stub_egress["proxy_arg"] == SOCKS - - -@pytest.mark.unit -def test_prepare_geo_egress_present_even_with_explicit_tz(stub_egress): - # explicit IANA zone still needs the egress for WebRTC behind a proxy. - geo = prepare_session_geo("Asia/Tokyo", SOCKS) - assert geo.timezone == "Asia/Tokyo" - assert geo.egress_ip == "203.0.113.7" - assert stub_egress["called"] is True - - -@pytest.mark.unit -def test_prepare_geo_no_egress_without_proxy(stub_egress): - # no proxy → no WebRTC override (real STUN already tells the truth). - geo = prepare_session_geo("auto", None) - assert geo.timezone == "America/New_York" - assert geo.egress_ip is None - - -@pytest.mark.unit -def test_prepare_geo_timezone_matches_resolve_session_timezone(stub_egress): - # the thin tz wrapper must stay equivalent to prepare_session_geo().timezone - for tz, proxy in [("Asia/Tokyo", SOCKS), ("auto", HTTP), ("", None)]: - assert prepare_session_geo(tz, proxy).timezone == resolve_session_timezone(tz, proxy) diff --git a/tests/test_geoip_update.py b/tests/test_geoip_update.py deleted file mode 100644 index 26632b7..0000000 --- a/tests/test_geoip_update.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Unit tests for the intelligent geoip mmdb auto-update in `download.py`. - -daijro/geoip-all-in-one rebuilds weekly; `ensure_geoip_mmdb` keeps the cache -fresh without a download (or API call) on every launch. These tests mock the -cache root, the latest-tag API, and the per-tag download so nothing touches the -network. -""" -import os -import time - -import pytest - -import invisible_playwright.download as dl - - -@pytest.fixture -def cache(tmp_path, monkeypatch): - """Point the cache at tmp_path and clear the env override.""" - monkeypatch.setattr(dl, "cache_root", lambda: tmp_path) - monkeypatch.delenv("STEALTHFOX_GEOIP_MMDB", raising=False) - return tmp_path - - -def _make_cached(root, tag, name=dl.GEOIP_MMDB_NAME): - d = root / "geoip" / tag - d.mkdir(parents=True, exist_ok=True) - f = d / name - f.write_bytes(b"FAKE-MMDB") - return f - - -def _set_marker_age(root, days): - m = root / "geoip" / ".last_check" - m.parent.mkdir(parents=True, exist_ok=True) - m.touch() - old = time.time() - days * 86400 - os.utime(m, (old, old)) - - -# ────────────────────────────────────────────────────────────────────── -# env override -# ────────────────────────────────────────────────────────────────────── -@pytest.mark.unit -def test_env_override_returns_file(tmp_path, monkeypatch): - f = tmp_path / "mine.mmdb" - f.write_bytes(b"X") - monkeypatch.setenv("STEALTHFOX_GEOIP_MMDB", str(f)) - assert dl.ensure_geoip_mmdb() == f - - -@pytest.mark.unit -def test_env_override_missing_raises(tmp_path, monkeypatch): - monkeypatch.setenv("STEALTHFOX_GEOIP_MMDB", str(tmp_path / "nope.mmdb")) - with pytest.raises(RuntimeError): - dl.ensure_geoip_mmdb() - - -# ────────────────────────────────────────────────────────────────────── -# freshness window -# ────────────────────────────────────────────────────────────────────── -@pytest.mark.unit -def test_fresh_cache_no_network(cache, monkeypatch): - f = _make_cached(cache, "2026.06.03") - _set_marker_age(cache, 0) # just checked - - def boom(): - raise AssertionError("latest-tag API must NOT be called within the window") - - monkeypatch.setattr(dl, "_latest_geoip_tag", boom) - assert dl.ensure_geoip_mmdb(max_age_days=7) == f - - -@pytest.mark.unit -def test_stale_same_tag_no_download(cache, monkeypatch): - f = _make_cached(cache, "2026.06.03") - _set_marker_age(cache, 30) # stale → will re-check - monkeypatch.setattr(dl, "_latest_geoip_tag", lambda: "2026.06.03") - # real _download_geoip_tag runs but target exists, so no actual download: - monkeypatch.setattr(dl, "_download_file", lambda *a, **k: (_ for _ in ()).throw( - AssertionError("must not download when tag already cached"))) - assert dl.ensure_geoip_mmdb(max_age_days=7) == f - - -@pytest.mark.unit -def test_stale_new_tag_downloads_and_prunes(cache, monkeypatch): - old = _make_cached(cache, "2026.06.03") - _set_marker_age(cache, 30) - monkeypatch.setattr(dl, "_latest_geoip_tag", lambda: "2026.06.10") - - def fake_download(tag): - return _make_cached(cache, tag) # simulate fetch+extract of the new tag - - monkeypatch.setattr(dl, "_download_geoip_tag", fake_download) - got = dl.ensure_geoip_mmdb(max_age_days=7) - assert got.parent.name == "2026.06.10" - assert not old.parent.exists() # old tag pruned - assert got.exists() - - -# ────────────────────────────────────────────────────────────────────── -# offline resilience -# ────────────────────────────────────────────────────────────────────── -@pytest.mark.unit -def test_api_down_with_cache_uses_cache(cache, monkeypatch): - f = _make_cached(cache, "2026.06.03") - _set_marker_age(cache, 30) - - def boom(): - raise OSError("offline") - - monkeypatch.setattr(dl, "_latest_geoip_tag", boom) - assert dl.ensure_geoip_mmdb(max_age_days=7) == f # stale cache reused, no raise - - -@pytest.mark.unit -def test_cold_cache_api_down_falls_back_to_pinned(cache, monkeypatch): - # no cache at all + API unreachable → pinned GEOIP_MMDB_VERSION fallback. - def boom(): - raise OSError("offline") - - monkeypatch.setattr(dl, "_latest_geoip_tag", boom) - captured = {} - - def fake_download(tag): - captured["tag"] = tag - return _make_cached(cache, tag) - - monkeypatch.setattr(dl, "_download_geoip_tag", fake_download) - got = dl.ensure_geoip_mmdb(max_age_days=7) - assert captured["tag"] == dl.GEOIP_MMDB_VERSION - assert got.exists() diff --git a/tests/test_headless.py b/tests/test_headless.py index d79b921..d979b34 100644 --- a/tests/test_headless.py +++ b/tests/test_headless.py @@ -1,39 +1,35 @@ -"""Unit tests for the ``_headless`` window-hider dispatcher. +"""Unit tests for the ``_headless`` virtual-display dispatcher. -``make_virtual_display`` is pure platform routing: -- Linux: a ``_LinuxVirtualDisplay`` (Xvfb) object the launcher start()s/stop()s. -- Windows / macOS: ``None`` — the patched binary self-cloaks its chrome windows - via ``cloak_prefs()`` (injected by the launcher), so nothing host-side spawns. -- Anything else: a clear ``RuntimeError`` naming the platform. +The dispatcher (``make_virtual_display``) is the only piece of +``_headless`` we can exercise as a unit test on a single platform: +``_WindowsVirtualDesktop`` actually creates a Win32 desktop on +construction's later ``start()`` call, and ``_LinuxVirtualDisplay`` calls +``Xvfb`` — both belong in integration/E2E coverage. The dispatcher's +job is pure platform routing, which we patch via ``monkeypatch``. -``_LinuxVirtualDisplay`` construction does no I/O (Xvfb is only spawned in -``start()``), so it's safe to exercise on any host. +Per scope: Windows-specific + platform-agnostic only. We still cover +the Linux dispatch branch because instantiating ``_LinuxVirtualDisplay`` +does no I/O — Xvfb is only spawned in ``start()``, which we never call. """ from __future__ import annotations +import sys + import pytest import invisible_playwright._headless as headless from invisible_playwright._headless import ( - CLOAK_PREFS, _LinuxVirtualDisplay, - cloak_prefs, + _WindowsVirtualDesktop, make_virtual_display, ) @pytest.mark.unit -def test_make_virtual_display_returns_none_on_win32(monkeypatch): - """Windows hides via the in-binary cloak pref, not a host-side display.""" +def test_make_virtual_display_returns_windows_desktop_on_win32(monkeypatch): monkeypatch.setattr(headless.sys, "platform", "win32") - assert make_virtual_display() is None - - -@pytest.mark.unit -def test_make_virtual_display_returns_none_on_darwin(monkeypatch): - """macOS is now supported — it hides via the same in-binary cloak pref.""" - monkeypatch.setattr(headless.sys, "platform", "darwin") - assert make_virtual_display() is None + vd = make_virtual_display() + assert isinstance(vd, _WindowsVirtualDesktop) @pytest.mark.unit @@ -41,7 +37,8 @@ def test_make_virtual_display_returns_linux_xvfb_on_linux(monkeypatch): """``__init__`` of ``_LinuxVirtualDisplay`` does no I/O — only ``start()`` spawns Xvfb. Exercising the dispatcher here is safe on any host.""" monkeypatch.setattr(headless.sys, "platform", "linux") - assert isinstance(make_virtual_display(), _LinuxVirtualDisplay) + vd = make_virtual_display() + assert isinstance(vd, _LinuxVirtualDisplay) @pytest.mark.unit @@ -52,10 +49,21 @@ def test_make_virtual_display_accepts_linux_variants(monkeypatch): assert isinstance(make_virtual_display(), _LinuxVirtualDisplay) +@pytest.mark.unit +def test_make_virtual_display_raises_on_darwin(monkeypatch): + """macOS is unsupported — the dispatcher must raise with a clear + message rather than returning a no-op shim. ``InvisiblePlaywright`` + relies on this to bail before launching Firefox on a system where + the patched binary doesn't exist.""" + monkeypatch.setattr(headless.sys, "platform", "darwin") + with pytest.raises(RuntimeError, match="Windows and Linux only"): + make_virtual_display() + + @pytest.mark.unit def test_make_virtual_display_raises_on_unsupported_platform(monkeypatch): monkeypatch.setattr(headless.sys, "platform", "freebsd14") - with pytest.raises(RuntimeError, match="Windows, macOS and Linux"): + with pytest.raises(RuntimeError, match="Windows and Linux only"): make_virtual_display() @@ -69,13 +77,32 @@ def test_make_virtual_display_error_mentions_offending_platform(monkeypatch): @pytest.mark.unit -def test_cloak_prefs_enables_cloak_and_disables_occlusion(): - """The cloak prefs must turn on the in-binary cloak and turn OFF Windows - occlusion tracking (so a hidden window keeps painting). Returns a copy.""" - p = cloak_prefs() - assert p["zoom.stealth.cloak_windows"] is True - assert p["widget.windows.window_occlusion_tracking.enabled"] is False - assert p == CLOAK_PREFS and p is not CLOAK_PREFS +def test_windows_desktop_initial_state_is_clean(): + """Construction must not allocate Win32 resources — only ``start()`` + does. Allows users to instantiate ``InvisiblePlaywright`` without + pywin32 installed; the import error fires lazily when ``start()`` runs.""" + vd = _WindowsVirtualDesktop() + assert vd._desktop is None + assert vd._original_handle == 0 + + +@pytest.mark.unit +@pytest.mark.skipif(sys.platform != "win32", reason="exercises Win32 ctypes") +def test_windows_desktop_stop_is_idempotent_without_start(): + """``stop()`` after never calling ``start()`` must be a no-op, so + ``__exit__`` from a failed launch can call it unconditionally. + + Skipped off Windows because ``stop()`` unconditionally resolves + ``ctypes.windll.user32`` at the top of the function — that symbol + only exists on Windows. The early-return logic is safe because + callers only instantiate this class via ``make_virtual_display()`` + which already routes on ``sys.platform == 'win32'``. + """ + vd = _WindowsVirtualDesktop() + vd.stop() + vd.stop() + assert vd._desktop is None + assert vd._original_handle == 0 # ────────────────────────────────────────────────────────────────────── diff --git a/tests/test_integration.py b/tests/test_integration.py index 7abd55f..1da7621 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -48,6 +48,7 @@ _REQUIRED_PREFS_KEYS = ( "intl.accept_languages", "general.useragent.locale", "intl.locale.requested", + "zoom.stealth.seed", "zoom.stealth.fpp.hw_seed", "zoom.stealth.webrtc.host_ip", "zoom.stealth.webgl.renderer", diff --git a/tests/test_launcher_config.py b/tests/test_launcher_config.py index 85047e5..daf88c4 100644 --- a/tests/test_launcher_config.py +++ b/tests/test_launcher_config.py @@ -55,217 +55,3 @@ def test_invisible_playwright_constructs_without_launching(): assert obj is not None obj2 = InvisiblePlaywright(seed=42, headless=True) assert obj2 is not None - - -# ─── profile_dir kwarg — persistent context support ─────────────────────── # - -import pytest -from pathlib import Path - - -@pytest.mark.unit -def test_profile_dir_none_by_default(): - """No persistent profile unless explicitly opted in. Prevents accidental - state-leak between scripts that share the same seed.""" - obj = InvisiblePlaywright(seed=42) - assert obj._profile_dir is None - assert obj._persistent_context is None - - -@pytest.mark.unit -def test_profile_dir_string_is_coerced_to_path(tmp_path): - """Accept str or Path. Always store as Path internally.""" - obj = InvisiblePlaywright(seed=42, profile_dir=str(tmp_path)) - assert isinstance(obj._profile_dir, Path) - assert obj._profile_dir == tmp_path - - -@pytest.mark.unit -def test_profile_dir_path_is_stored_as_is(tmp_path): - obj = InvisiblePlaywright(seed=42, profile_dir=tmp_path) - assert obj._profile_dir == tmp_path - - -@pytest.mark.unit -def test_profile_dir_does_not_create_dir_until_enter(tmp_path): - """Construction must not touch the filesystem. Directory creation only - happens when the user actually enters the context manager — otherwise - a typo at instantiation would silently spawn dirs.""" - target = tmp_path / "nonexistent" - assert not target.exists() - InvisiblePlaywright(seed=42, profile_dir=target) - assert not target.exists() - - -@pytest.mark.unit -def test_persistent_context_kwargs_match_default_exactly(): - """Persistent kwargs must be IDENTICAL to non-persistent default - kwargs. From firefox-5 (C7 closure) the docShell.overrideTimezone - method is present in the patched binary, so the per-realm overrides - Playwright applies for `locale=`/`timezone_id=` land successfully and - no longer hang the persistent context launch handshake. - - Before firefox-5 we had to filter these out (180s timeout otherwise). - A future refactor that re-introduces that filter would silently lose - timezone/locale isolation in persistent sessions — this test is the - sentinel that catches the regression at the unit level.""" - obj = InvisiblePlaywright(seed=42, locale="en-GB", timezone="Europe/London", - profile_dir="/tmp/x") - persistent = obj._persistent_context_kwargs() - default = obj._default_context_kwargs() - assert persistent == default, ( - "persistent_context kwargs must match default_context kwargs since " - f"firefox-5.\n persistent: {persistent!r}\n default: {default!r}" - ) - - -@pytest.mark.unit -def test_persistent_context_kwargs_INCLUDES_locale_and_timezone(): - """Sentinel for the C7 closure: firefox-5 ships the C++ overrideTimezone - IDL method, so locale + timezone_id MUST be passed through to - launch_persistent_context. If they're not, the wrapper is silently - dropping per-context isolation — two sessions with different - `timezone=` would end up sharing whatever TZ the env var set. - - Regression-defense: do NOT re-add the firefox-4-era filter.""" - obj = InvisiblePlaywright(seed=42, locale="en-GB", timezone="Europe/London", - profile_dir="/tmp/x") - kw = obj._persistent_context_kwargs() - assert kw.get("locale") == "en-GB", ( - f"locale must be in persistent kwargs (firefox-5+ supports it via " - f"docShell.languageOverride). Got: {kw.get('locale')!r}" - ) - assert kw.get("timezone_id") == "Europe/London", ( - f"timezone_id must be in persistent kwargs (firefox-5+ supports it " - f"via docShell.overrideTimezone IDL method, patch.md section 19). " - f"Got: {kw.get('timezone_id')!r}" - ) - - -@pytest.mark.unit -def test_persistent_context_kwargs_omits_timezone_when_empty_string(): - """Empty timezone='' is the 'use host TZ' sentinel — must NOT pass - timezone_id to Playwright in that case (would pin to literal '' and - break Intl).""" - obj = InvisiblePlaywright(seed=42, timezone="", profile_dir="/tmp/x") - kw = obj._persistent_context_kwargs() - assert "timezone_id" not in kw - - -# ─── Mocked __enter__ flow — confirms the right Playwright call is made ── # - - -@pytest.mark.unit -def test_enter_with_profile_dir_calls_launch_persistent_context(tmp_path, monkeypatch): - """When profile_dir is set, __enter__ must call - `firefox.launch_persistent_context(user_data_dir=...)` and NOT - `firefox.launch(...)`. This is the structural test that the persistent - branch is wired correctly — without it, profile_dir would be silently - accepted but ignored.""" - from unittest.mock import MagicMock - # Mock ensure_binary so we don't hit the network - monkeypatch.setattr("invisible_playwright.launcher.ensure_binary", - lambda: tmp_path / "firefox") - - # Mock sync_playwright().start() → fake playwright with our recording firefox - fake_ctx = MagicMock(name="persistent_context") - fake_firefox = MagicMock() - fake_firefox.launch_persistent_context.return_value = fake_ctx - fake_playwright = MagicMock() - fake_playwright.firefox = fake_firefox - fake_pw = MagicMock() - fake_pw.start.return_value = fake_playwright - - monkeypatch.setattr("invisible_playwright.launcher.sync_playwright", - lambda: fake_pw) - - profile = tmp_path / "myprofile" - obj = InvisiblePlaywright(seed=42, profile_dir=profile) - returned = obj.__enter__() - - # The persistent branch was taken - fake_firefox.launch_persistent_context.assert_called_once() - fake_firefox.launch.assert_not_called() - - # The user_data_dir was passed verbatim - call_kwargs = fake_firefox.launch_persistent_context.call_args.kwargs - assert call_kwargs["user_data_dir"] == str(profile) - - # The directory was created on disk (Playwright fails otherwise) - assert profile.exists() and profile.is_dir() - - # __enter__ returned the BrowserContext, not a Browser - assert returned is fake_ctx - - -@pytest.mark.unit -def test_enter_without_profile_dir_calls_launch_not_persistent(tmp_path, monkeypatch): - """Default path: profile_dir=None → firefox.launch, not - launch_persistent_context. Sentinel that the non-persistent flow - isn't accidentally rerouted.""" - from unittest.mock import MagicMock - monkeypatch.setattr("invisible_playwright.launcher.ensure_binary", - lambda: tmp_path / "firefox") - - fake_browser = MagicMock(name="browser") - fake_browser.new_context = MagicMock() - fake_firefox = MagicMock() - fake_firefox.launch.return_value = fake_browser - fake_playwright = MagicMock() - fake_playwright.firefox = fake_firefox - fake_pw = MagicMock() - fake_pw.start.return_value = fake_playwright - - monkeypatch.setattr("invisible_playwright.launcher.sync_playwright", - lambda: fake_pw) - - obj = InvisiblePlaywright(seed=42) - returned = obj.__enter__() - - fake_firefox.launch.assert_called_once() - fake_firefox.launch_persistent_context.assert_not_called() - assert returned is fake_browser - - -@pytest.mark.unit -def test_persistent_context_user_data_dir_is_created_if_missing(tmp_path, monkeypatch): - """First-run scenario: the directory the user names doesn't exist yet. - __enter__ must mkdir -p it (Playwright won't, and would crash with - 'user_data_dir does not exist').""" - from unittest.mock import MagicMock - monkeypatch.setattr("invisible_playwright.launcher.ensure_binary", - lambda: tmp_path / "firefox") - fake_pw = MagicMock() - fake_pw.start.return_value = MagicMock() - fake_pw.start.return_value.firefox.launch_persistent_context = MagicMock( - return_value=MagicMock() - ) - monkeypatch.setattr("invisible_playwright.launcher.sync_playwright", - lambda: fake_pw) - - nested = tmp_path / "a" / "b" / "c" / "profile" - assert not nested.parent.exists() # parent doesn't exist either - obj = InvisiblePlaywright(seed=42, profile_dir=nested) - obj.__enter__() - assert nested.is_dir() - - -@pytest.mark.unit -def test_teardown_closes_persistent_context(tmp_path, monkeypatch): - """The teardown must close the persistent context. Forgetting this - leaves Firefox + Playwright running until the parent process exits, - which on long-running tools (job orchestrators, MCP servers) leaks - handles indefinitely.""" - from unittest.mock import MagicMock - monkeypatch.setattr("invisible_playwright.launcher.ensure_binary", - lambda: tmp_path / "firefox") - fake_ctx = MagicMock(name="persistent_context") - fake_pw = MagicMock() - fake_pw.start.return_value.firefox.launch_persistent_context.return_value = fake_ctx - monkeypatch.setattr("invisible_playwright.launcher.sync_playwright", - lambda: fake_pw) - - obj = InvisiblePlaywright(seed=42, profile_dir=tmp_path / "p") - obj.__enter__() - obj.__exit__(None, None, None) - fake_ctx.close.assert_called_once() diff --git a/tests/test_launcher_helpers.py b/tests/test_launcher_helpers.py index 590736a..5122e88 100644 --- a/tests/test_launcher_helpers.py +++ b/tests/test_launcher_helpers.py @@ -169,38 +169,3 @@ def test_default_context_includes_locale_when_set(): def test_default_context_omits_locale_when_empty(): ip = InvisiblePlaywright(seed=42, locale="") assert "locale" not in ip._default_context_kwargs() - - -# ── InvisiblePlaywright._build_env — WebRTC egress auto-derive ───────── -# Locks the 2026-06-10 fix: behind a proxy the launcher feeds the discovered -# egress IP to nICEr (srflx override) + drops IPv6. Without it, a proxied -# session's WebRTC silently fell back to leaking/blocking. Runs in tests.yml. - - -@pytest.mark.unit -def test_build_env_injects_webrtc_egress_when_discovered(): - ip = InvisiblePlaywright(seed=42) - ip._webrtc_egress_ip = "203.0.113.9" # what __enter__ resolves behind a proxy - env = ip._build_env() - assert env["STEALTHFOX_WEBRTC_PUBLIC_IP"] == "203.0.113.9" - assert env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] == "1" - - -@pytest.mark.unit -def test_build_env_no_webrtc_keys_without_proxy(monkeypatch): - monkeypatch.delenv("STEALTHFOX_WEBRTC_PUBLIC_IP", raising=False) - ip = InvisiblePlaywright(seed=42) - ip._webrtc_egress_ip = None # no proxy → real STUN already truthful - env = ip._build_env() - assert "STEALTHFOX_WEBRTC_PUBLIC_IP" not in env - assert "STEALTHFOX_WEBRTC_DISABLE_IPV6" not in env - - -@pytest.mark.unit -def test_build_env_caller_env_override_wins(monkeypatch): - monkeypatch.setenv("STEALTHFOX_WEBRTC_PUBLIC_IP", "198.51.100.5") - ip = InvisiblePlaywright(seed=42) - ip._webrtc_egress_ip = "203.0.113.9" # auto-discovered - env = ip._build_env() - assert env["STEALTHFOX_WEBRTC_PUBLIC_IP"] == "198.51.100.5" # caller wins - assert env["STEALTHFOX_WEBRTC_DISABLE_IPV6"] == "1" diff --git a/tests/test_mouse.py b/tests/test_mouse.py index 03d3206..ad0f00e 100644 --- a/tests/test_mouse.py +++ b/tests/test_mouse.py @@ -16,11 +16,24 @@ and covers each patched call site: """ from __future__ import annotations +import sys import urllib.parse import pytest from invisible_playwright import InvisiblePlaywright +from invisible_playwright.constants import BINARY_ENTRY_REL + + +@pytest.fixture(scope="session") +def firefox_binary(): + if sys.platform not in BINARY_ENTRY_REL: + pytest.skip(f"unsupported platform: {sys.platform}") + from invisible_playwright.download import cache_dir_for_version + entry = cache_dir_for_version() / BINARY_ENTRY_REL[sys.platform] + if not entry.exists(): + pytest.skip("patched Firefox binary not cached; run `invisible-playwright fetch`") + return str(entry) def _data_url(html: str) -> str: @@ -132,9 +145,12 @@ def test_mouse_move_outside_viewport_does_not_raise(firefox_binary): # ──────────────────────────────────────────────────────────────────── -def _humanize_move_count(firefox_binary, humanize): - """Count page mousemove events fired by ONE long mouse.move.""" - with InvisiblePlaywright(seed=42, binary_path=firefox_binary, humanize=humanize) as browser: +@pytest.mark.e2e +def test_humanize_emits_intermediate_moves(firefox_binary): + """A long mouse.move from one corner to another should fire several + mousemove events on the page when the humanize hook is enabled (which + is the StealthFox default).""" + with InvisiblePlaywright(seed=42, binary_path=firefox_binary) as browser: page = browser.new_page() page.goto(_data_url( "
= 1` alone was a false-green — a teleport already fires 1 — - and that false-green hid a pref-namespace bug (wrapper wrote - `invisible_playwright.humanize`, the binary's Juggler reads `stealthfox.humanize`) - that left humanize silently dead in production. This test now fails if the - pref ever stops reaching the binary.""" - on = _humanize_move_count(firefox_binary, True) - off = _humanize_move_count(firefox_binary, False) - assert off <= 2, f"humanize OFF should ~teleport (<=2 moves), got {off}" - assert on >= 4, ( - f"humanize ON must expand into many intermediate moves (Bezier); got {on} " - f"(off={off}). moves==1 means the cursor teleports — the exact automation " - f"tell humanize exists to remove, and a sign the stealthfox.* pref isn't " - f"reaching the binary's Juggler." - ) + moves = page.evaluate("window.__n") + assert moves >= 1, f"expected at least 1 mousemove event, got {moves}" # ──────────────────────────────────────────────────────────────────── @@ -198,14 +195,7 @@ def test_hover_triggers_mouseenter(firefox_binary): "onmouseenter=\"window.__h=true\">x
" )) page.locator("#h").hover() - # Wait for the event rather than reading immediately: under load / on a - # virtual display the mouseenter can land a beat after hover() returns, - # which made an instant read flaky. wait_for_function still fails (times - # out) if mouseenter genuinely never fires. Timeout is generous (10s) so a - # busy full-suite run — where browser startup + CPU contention can push - # the event past a tight 5s window — doesn't flake; the event itself fires - # in well under a second when run in isolation. - page.wait_for_function("() => window.__h === true", timeout=10_000) + assert page.evaluate("window.__h") is True # ──────────────────────────────────────────────────────────────────── diff --git a/tests/test_prefs.py b/tests/test_prefs.py index ae088c8..fa27345 100644 --- a/tests/test_prefs.py +++ b/tests/test_prefs.py @@ -158,22 +158,21 @@ def test_webgl_extensions_cleared_on_windows(monkeypatch): @pytest.mark.unit -def test_timezone_set_uses_juggler_pref(): - # TZ1 — juggler.timezone.override is the sole C++-read timezone pref; - # the old zoom.stealth.timezone alias (orphan) must NOT be reintroduced. +def test_timezone_set_propagates_to_both_keys(): + # TZ1 p = generate_profile(seed=42) prefs = translate_profile_to_prefs(p, timezone="America/New_York") + assert prefs["zoom.stealth.timezone"] == "America/New_York" assert prefs["juggler.timezone.override"] == "America/New_York" - assert "zoom.stealth.timezone" not in prefs @pytest.mark.unit -def test_timezone_empty_omits_the_key(): +def test_timezone_empty_omits_both_keys(): # TZ2 p = generate_profile(seed=42) prefs = translate_profile_to_prefs(p, timezone="") - assert "juggler.timezone.override" not in prefs assert "zoom.stealth.timezone" not in prefs + assert "juggler.timezone.override" not in prefs # ────────────────────────────────────────────────────────────────────── @@ -201,10 +200,10 @@ def test_extra_prefs_none_value_deletes_key(): @pytest.mark.unit def test_extra_prefs_overrides_existing_key(): - # EP3 — override a real baseline key (hw_seed is the live cross-process seed) + # EP3 p = generate_profile(seed=42) - prefs = translate_profile_to_prefs(p, extra_prefs={"zoom.stealth.fpp.hw_seed": 999}) - assert prefs["zoom.stealth.fpp.hw_seed"] == 999 + prefs = translate_profile_to_prefs(p, extra_prefs={"zoom.stealth.seed": 999}) + assert prefs["zoom.stealth.seed"] == 999 @pytest.mark.unit diff --git a/tests/test_proxy_socks_auth_e2e.py b/tests/test_proxy_socks_auth_e2e.py deleted file mode 100644 index 2d8fafa..0000000 --- a/tests/test_proxy_socks_auth_e2e.py +++ /dev/null @@ -1,197 +0,0 @@ -"""E2E: the patched Firefox SENDS SOCKS5 username/password and routes through it. - -Playwright's own ``proxy=`` ignores SOCKS auth; this is the patched -``nsProtocolProxyService`` feature (reads ``network.proxy.socks_username`` / -``socks_password``). ``test_proxy.py`` already unit-tests on CI that the wrapper -sets those prefs; this proves the binary actually performs the RFC1929 auth -handshake and relays traffic. - -Fully hermetic — a local SOCKS5 server + a local HTTP target, with the localhost -target forced through the proxy via ``allow_hijacking_localhost`` — so it runs -identically on a dev box and on a GitHub runner (no external site, no secrets). -""" -from __future__ import annotations - -import http.server -import socket -import socketserver -import struct -import threading - -import pytest - -from invisible_playwright import InvisiblePlaywright - -_USER = "ferd_socks_user" -_PASS = "ferd_socks_pw_42" - - -class _Socks5AuthRecorder: - """SOCKS5 that REQUIRES RFC1929 user/pass auth, records the creds it saw, - then relays CONNECT to the requested target.""" - - def __init__(self): - self._srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self._srv.bind(("127.0.0.1", 0)) - self._srv.listen(16) - self.port = self._srv.getsockname()[1] - self.seen_creds: list[tuple[str, str]] = [] - self._stop = False - threading.Thread(target=self._serve, daemon=True).start() - - def _serve(self): - while not self._stop: - try: - conn, _ = self._srv.accept() - except OSError: - break - threading.Thread(target=self._handle, args=(conn,), daemon=True).start() - - def _recv(self, s, n): - buf = b"" - while len(buf) < n: - chunk = s.recv(n - len(buf)) - if not chunk: - return None - buf += chunk - return buf - - def _handle(self, conn): - try: - head = self._recv(conn, 2) - if not head or head[0] != 0x05: - conn.close(); return - methods = self._recv(conn, head[1]) or b"" - if 0x02 not in methods: # we REQUIRE user/pass - conn.sendall(b"\x05\xff"); conn.close(); return - conn.sendall(b"\x05\x02") # select user/pass auth - if not self._recv(conn, 1): # RFC1929 version byte - conn.close(); return - ulen = self._recv(conn, 1)[0] - uname = (self._recv(conn, ulen) or b"").decode("utf-8", "ignore") - plen = self._recv(conn, 1)[0] - passwd = (self._recv(conn, plen) or b"").decode("utf-8", "ignore") - self.seen_creds.append((uname, passwd)) - conn.sendall(b"\x01\x00") # auth success - req = self._recv(conn, 4) - if not req: - conn.close(); return - _, cmd, _, atyp = req - if atyp == 0x01: - addr = socket.inet_ntoa(self._recv(conn, 4)) - elif atyp == 0x03: - addr = (self._recv(conn, self._recv(conn, 1)[0]) or b"").decode() - elif atyp == 0x04: - addr = socket.inet_ntop(socket.AF_INET6, self._recv(conn, 16)) - else: - conn.close(); return - port = struct.unpack("!H", self._recv(conn, 2))[0] - if cmd != 0x01: # only CONNECT - conn.sendall(b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00"); conn.close(); return - try: - up = socket.create_connection((addr, port), timeout=15) - except OSError: - conn.sendall(b"\x05\x05\x00\x01\x00\x00\x00\x00\x00\x00"); conn.close(); return - conn.sendall(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00") - self._pipe(conn, up) - except Exception: - try: - conn.close() - except OSError: - pass - - @staticmethod - def _pipe(a, b): - def fwd(src, dst): - try: - while True: - data = src.recv(65536) - if not data: - break - dst.sendall(data) - except OSError: - pass - finally: - try: - dst.shutdown(socket.SHUT_WR) - except OSError: - pass - threading.Thread(target=fwd, args=(a, b), daemon=True).start() - fwd(b, a) - - def close(self): - self._stop = True - try: - self._srv.close() - except OSError: - pass - - -class _LocalHTTP: - """A tiny localhost HTTP server — the CONNECT target relayed by the proxy.""" - - _HTML = b"ok

socks-routed

" - - def __init__(self): - html = self._HTML - - class H(http.server.BaseHTTPRequestHandler): - def do_GET(self): # noqa: N802 - self.send_response(200) - self.send_header("Content-Type", "text/html; charset=utf-8") - self.send_header("Content-Length", str(len(html))) - self.end_headers() - self.wfile.write(html) - - def log_message(self, *a): - pass - - self._srv = socketserver.TCPServer(("127.0.0.1", 0), H) - self.port = self._srv.server_address[1] - threading.Thread(target=self._srv.serve_forever, daemon=True).start() - - def close(self): - self._srv.shutdown() - - -@pytest.fixture -def socks_auth(): - s = _Socks5AuthRecorder() - yield s - s.close() - - -@pytest.fixture -def local_http(): - h = _LocalHTTP() - yield h - h.close() - - -@pytest.mark.e2e -def test_socks5_auth_creds_sent_and_routed(firefox_binary, socks_auth, local_http): - """The binary must perform SOCKS5 user/pass auth with the configured creds - and relay the page through the proxy.""" - proxy = { - "server": f"socks5://127.0.0.1:{socks_auth.port}", - "username": _USER, - "password": _PASS, - } - # Firefox bypasses the proxy for localhost by default; force it through. - prefs = { - "network.proxy.allow_hijacking_localhost": True, - "network.proxy.no_proxies_on": "", - } - with InvisiblePlaywright( - seed=42, binary_path=firefox_binary, proxy=proxy, extra_prefs=prefs - ) as browser: - page = browser.new_page() - page.goto(f"http://127.0.0.1:{local_http.port}/", wait_until="load", timeout=30000) - text = page.evaluate("() => document.getElementById('ok').textContent") - - assert text == "socks-routed", "page did not load through the SOCKS proxy" - assert (_USER, _PASS) in socks_auth.seen_creds, ( - f"patched Firefox did not send the SOCKS5 auth creds from prefs; " - f"proxy saw: {socks_auth.seen_creds!r}" - ) diff --git a/tests/test_recaptcha_seed.py b/tests/test_recaptcha_seed.py deleted file mode 100644 index dbd1821..0000000 --- a/tests/test_recaptcha_seed.py +++ /dev/null @@ -1,349 +0,0 @@ -"""Unit tests for the deterministic reCAPTCHA cookie builder. - -Validates the contract: - - 6 .google.com cookies always present - - Per-site cookies built from a `browsing_history` list (sampled by the - Bayesian network in _fpforge) - - Determinism: same (seed, history) → identical content - - Chrome 400-day cookie cap respected - - Playwright add_cookies field requirements satisfied -""" -import pytest - -from invisible_playwright._recaptcha_seed import ( - build_cookies, - _sub_seed, -) - - -pytestmark = pytest.mark.unit - - -_FIXED_NOW = 1779600000 # 2026-05-23, frozen for determinism - - -# Sample browsing history for tests (mimics what _fpforge produces). -_SAMPLE_HISTORY = [ - {"name": "github.com", "category": "dev", "cookie_profile": "ga_cf"}, - {"name": "stackoverflow.com", "category": "dev", "cookie_profile": "ga_consent_clarity"}, - {"name": "amazon.com", "category": "shop", "cookie_profile": "ga_consent_clarity"}, - {"name": "wikipedia.org", "category": "reference", "cookie_profile": "minimal"}, - {"name": "youtube.com", "category": "media", "cookie_profile": "ga_only"}, -] - - -# =========================================================================== -# 1. Set composition -# =========================================================================== - -def test_only_google_cookies_when_no_history(): - """Empty/None history → only the 5 .google.com cookies (1P_JAR removed - in realism round 2 — deprecated by Google 2022).""" - cookies = build_cookies(seed=42, browsing_history=None, now=_FIXED_NOW) - names = sorted(c["name"] for c in cookies) - assert names == sorted(["NID", "CONSENT", "SOCS", - "_GRECAPTCHA", "ENID"]) - assert all(c["domain"] == ".google.com" for c in cookies) - - -def test_browsing_history_adds_host_cookies(): - """Each history site contributes 1+ cookies on its domain.""" - cookies = build_cookies(seed=42, browsing_history=_SAMPLE_HISTORY, now=_FIXED_NOW) - google = [c for c in cookies if c["domain"] == ".google.com"] - assert len(google) == 5 # 1P_JAR removed - - domains = {c["domain"] for c in cookies if c["domain"] != ".google.com"} - for site in _SAMPLE_HISTORY: - assert f".{site['name']}" in domains - - -def test_domain_dot_prefix_normalized(): - """All host cookie domains have a leading dot for sub-domain coverage.""" - cookies = build_cookies(seed=42, browsing_history=_SAMPLE_HISTORY, now=_FIXED_NOW) - for c in cookies: - assert c["domain"].startswith("."), f"missing dot: {c['domain']}" - - -# =========================================================================== -# 2. Cookie profile recipes (each profile yields the expected cookie set) -# =========================================================================== - -def test_profile_minimal_yields_ga_only(): - history = [{"name": "x.com", "cookie_profile": "minimal"}] - cookies = build_cookies(seed=42, browsing_history=history, now=_FIXED_NOW) - host = [c for c in cookies if c["domain"] == ".x.com"] - names = [c["name"] for c in host] - assert names == ["_ga"] - - -def test_profile_ga_only_yields_ga_and_gid(): - history = [{"name": "x.com", "cookie_profile": "ga_only"}] - cookies = build_cookies(seed=42, browsing_history=history, now=_FIXED_NOW) - host = [c for c in cookies if c["domain"] == ".x.com"] - names = sorted(c["name"] for c in host) - assert names == ["_ga", "_gid"] - - -def test_profile_ga_cf_yields_ga_and_cf_bm(): - history = [{"name": "x.com", "cookie_profile": "ga_cf"}] - cookies = build_cookies(seed=42, browsing_history=history, now=_FIXED_NOW) - host = [c for c in cookies if c["domain"] == ".x.com"] - names = sorted(c["name"] for c in host) - assert names == ["__cf_bm", "_ga"] - - -def test_profile_ga_consent_yields_three_cookies(): - history = [{"name": "x.com", "cookie_profile": "ga_consent"}] - cookies = build_cookies(seed=42, browsing_history=history, now=_FIXED_NOW) - host = [c for c in cookies if c["domain"] == ".x.com"] - names = sorted(c["name"] for c in host) - # Always _ga + _gid + one of OneTrust|CookieYes - assert "_ga" in names and "_gid" in names - assert any(n in names for n in ("OptanonAlertBoxClosed", "cookieyes-consent")) - assert len(host) == 3 - - -def test_profile_ga_consent_clarity_yields_at_least_four_cookies(): - """Always _ga + _gid + _clck + consent banner. Optionally _fbp, _dc_gtm_*, - __hssrc (probabilistic per rng — see test_new_helper_cookies_*).""" - history = [{"name": "x.com", "cookie_profile": "ga_consent_clarity"}] - cookies = build_cookies(seed=42, browsing_history=history, now=_FIXED_NOW) - host = [c for c in cookies if c["domain"] == ".x.com"] - names = sorted(c["name"] for c in host) - assert "_ga" in names and "_gid" in names and "_clck" in names - assert any(n in names for n in ("OptanonAlertBoxClosed", "cookieyes-consent")) - assert len(host) >= 4 # 4 baseline + 0-3 helpers - - -def test_unknown_profile_falls_back_to_ga(): - history = [{"name": "x.com", "cookie_profile": "nonexistent_profile"}] - cookies = build_cookies(seed=42, browsing_history=history, now=_FIXED_NOW) - host = [c for c in cookies if c["domain"] == ".x.com"] - assert [c["name"] for c in host] == ["_ga"] - - -# =========================================================================== -# 3. Determinism -# =========================================================================== - -def test_same_seed_and_history_same_content(): - a = build_cookies(seed=42, browsing_history=_SAMPLE_HISTORY, now=_FIXED_NOW) - b = build_cookies(seed=42, browsing_history=_SAMPLE_HISTORY, now=_FIXED_NOW) - assert a == b - - -def test_different_seed_different_content(): - a = build_cookies(seed=42, browsing_history=_SAMPLE_HISTORY, now=_FIXED_NOW) - b = build_cookies(seed=99, browsing_history=_SAMPLE_HISTORY, now=_FIXED_NOW) - a_nid = next(c for c in a if c["name"] == "NID")["value"] - b_nid = next(c for c in b if c["name"] == "NID")["value"] - assert a_nid != b_nid - - -def test_history_order_does_not_affect_domain_specific_cookies(): - """Sub-seed is keyed on domain name, not order in history list.""" - h1 = [_SAMPLE_HISTORY[0], _SAMPLE_HISTORY[1]] - h2 = [_SAMPLE_HISTORY[1], _SAMPLE_HISTORY[0]] - a = {(c["domain"], c["name"]): c["value"] - for c in build_cookies(seed=42, browsing_history=h1, now=_FIXED_NOW) - if c["domain"] != ".google.com"} - b = {(c["domain"], c["name"]): c["value"] - for c in build_cookies(seed=42, browsing_history=h2, now=_FIXED_NOW) - if c["domain"] != ".google.com"} - assert a == b - - -def test_sub_seed_distinct_tags_distinct_streams(): - assert _sub_seed(42, "google") != _sub_seed(42, "dom:github.com") - assert _sub_seed(42, "dom:github.com") != _sub_seed(42, "dom:amazon.com") - assert _sub_seed(0, "any") != 0 # seed=0 still produces non-zero sub-seed - - -# =========================================================================== -# 4. Format / structural correctness for the Google batch -# =========================================================================== - -def test_nid_format(): - cookies = build_cookies(seed=42, now=_FIXED_NOW) - nid = next(c for c in cookies if c["name"] == "NID") - prefix, b64 = nid["value"].split("=", 1) - assert prefix.isdigit() and len(prefix) == 3 - # Broadened to 100-540 in realism round 2 to cover historical NID versions - assert 100 <= int(prefix) <= 540 - assert len(b64) == 178 - - -def test_consent_format(): - cookies = build_cookies(seed=42, now=_FIXED_NOW) - consent = next(c for c in cookies if c["name"] == "CONSENT") - assert consent["value"].startswith("YES+cb.") - assert "+FX+" in consent["value"] - - -# =========================================================================== -# 5. Chrome 400-day cookie cap compliance -# =========================================================================== - -def test_all_expiries_within_400_day_cap(): - """Chrome 104+ caps cookie expiry to 400 days. Cookies > 400d silently - truncated / dropped. We tighten everything to <=395d (except __cf_bm - which is short-lived telemetry).""" - cookies = build_cookies(seed=42, browsing_history=_SAMPLE_HISTORY, now=_FIXED_NOW) - max_allowed = _FIXED_NOW + 400 * 86400 - for c in cookies: - # Short-lived telemetry cookies are fine - if c["name"] in ("__cf_bm", "1P_JAR", "_gid"): - continue - assert c["expires"] <= max_allowed, ( - f"Cookie {c['name']} expires {c['expires'] - _FIXED_NOW}s " - f"(> 400d cap) — would be silently dropped" - ) - - -# =========================================================================== -# 6. Playwright add_cookies field requirements -# =========================================================================== - -def test_all_cookies_have_required_playwright_fields(): - cookies = build_cookies(seed=42, browsing_history=_SAMPLE_HISTORY, now=_FIXED_NOW) - for c in cookies: - assert c.get("name"), f"missing name: {c}" - assert c.get("value") is not None, f"missing value: {c}" - assert c.get("domain"), f"missing domain: {c}" - assert c.get("path") == "/", f"path != / for {c['name']}" - - -def test_modern_cookies_marked_secure(): - """Cookies with sameSite=None require secure=True under Firefox/Chrome. - Also generally needed for cookies set via Playwright add_cookies without - a navigation context.""" - cookies = build_cookies(seed=42, browsing_history=_SAMPLE_HISTORY, now=_FIXED_NOW) - for c in cookies: - if c.get("sameSite") == "None": - assert c.get("secure") is True, f"{c['name']} None+!secure invalid" - - -def test_httponly_on_signed_cookies(): - cookies = build_cookies(seed=42, now=_FIXED_NOW) - nid = next(c for c in cookies if c["name"] == "NID") - enid = next(c for c in cookies if c["name"] == "ENID") - assert nid.get("httpOnly") is True - assert enid.get("httpOnly") is True - - -# =========================================================================== -# 7. End-to-end with real fpforge Profile -# =========================================================================== - -def test_with_real_fpforge_profile(): - """End-to-end: generate a real Profile, ensure browsing_history is populated - and build_cookies works against it.""" - from invisible_playwright._fpforge import generate_profile - prof = generate_profile(seed=42) - assert isinstance(prof.browsing_history, list) - # The Bayesian network samples ~15-30 sites per persona - assert 5 <= len(prof.browsing_history) <= 50, \ - f"unexpected history length: {len(prof.browsing_history)}" - # Each entry has the expected fields - for site in prof.browsing_history: - assert "name" in site and "category" in site and "cookie_profile" in site - # build_cookies works against the real profile - cookies = build_cookies(seed=prof.seed, browsing_history=prof.browsing_history, - now=_FIXED_NOW) - # 6 google + at least 1 cookie per visited site - assert len(cookies) >= 6 + len(prof.browsing_history) - - -def test_same_seed_same_browsing_history_via_fpforge(): - """Profile.browsing_history is deterministic from seed (Bayesian sampler).""" - from invisible_playwright._fpforge import generate_profile - a = generate_profile(seed=42).browsing_history - b = generate_profile(seed=42).browsing_history - assert a == b - - -# =========================================================================== -# 8. Realism improvements (2026-05-24 round 2) -# =========================================================================== - -def test_no_1p_jar_cookie(): - """1P_JAR was deprecated by Google in 2022. Including it is an - anachronism flag for fingerprinters that look at cookie freshness.""" - cookies = build_cookies(seed=42, browsing_history=_SAMPLE_HISTORY, now=_FIXED_NOW) - names = {c["name"] for c in cookies} - assert "1P_JAR" not in names - - -def test_nid_prefix_broadened_range(): - """NID 3-digit prefix should cover historical versions (137/105/511/525 - seen in real captures) — range 100-540, not just 500-540.""" - seen_prefixes = set() - for seed in range(200): - cookies = build_cookies(seed=seed, now=_FIXED_NOW) - nid = next(c for c in cookies if c["name"] == "NID") - prefix = int(nid["value"].split("=", 1)[0]) - seen_prefixes.add(prefix) - assert min(seen_prefixes) < 500, f"NID range never goes below 500 ({sorted(seen_prefixes)[:5]})" - assert max(seen_prefixes) <= 540 - - -def test_consent_lang_from_timezone_eu(): - """CONSENT cookie's `lang+region` token derived from IANA timezone.""" - cookies = build_cookies(seed=42, now=_FIXED_NOW, timezone="Europe/Rome") - consent = next(c for c in cookies if c["name"] == "CONSENT") - assert ".it+IT+" in consent["value"], f"expected it+IT in: {consent['value']}" - - -def test_consent_lang_default_fx(): - """Unknown / US timezone → default `en+FX` (non-EU fallback).""" - cookies = build_cookies(seed=42, now=_FIXED_NOW, timezone="America/New_York") - consent = next(c for c in cookies if c["name"] == "CONSENT") - assert ".en+FX+" in consent["value"] - - -def test_consent_lang_de_for_berlin(): - cookies = build_cookies(seed=42, now=_FIXED_NOW, timezone="Europe/Berlin") - consent = next(c for c in cookies if c["name"] == "CONSENT") - assert ".de+DE+" in consent["value"] - - -def test_consent_lang_no_timezone_default(): - """timezone=None → default en+FX.""" - cookies = build_cookies(seed=42, now=_FIXED_NOW) - consent = next(c for c in cookies if c["name"] == "CONSENT") - assert ".en+FX+" in consent["value"] - - -def test_new_helper_cookies_appear_in_ga_consent_clarity(): - """ga_consent_clarity recipe should sometimes include _fbp, _dc_gtm_*, __hssrc - (probabilistic per rng). Check across many seeds that they appear.""" - saw_fbp = False - saw_gtm = False - saw_hssrc = False - history = [{"name": "site.com", "cookie_profile": "ga_consent_clarity"}] - for seed in range(100): - cookies = build_cookies(seed=seed, browsing_history=history, now=_FIXED_NOW) - names = {c["name"] for c in cookies if c["domain"] == ".site.com"} - if "_fbp" in names: saw_fbp = True - if any(n.startswith("_dc_gtm_") for n in names): saw_gtm = True - if "__hssrc" in names: saw_hssrc = True - assert saw_fbp, "_fbp never appeared in 100 seeds (rng pick broken)" - assert saw_gtm, "_dc_gtm_* never appeared in 100 seeds" - assert saw_hssrc, "__hssrc never appeared in 100 seeds" - - -def test_fbp_format(): - """_fbp format: fb...""" - history = [{"name": "x.com", "cookie_profile": "ga_consent_clarity"}] - # Try multiple seeds until we hit a seed that includes _fbp (50% chance) - for seed in range(20): - cookies = build_cookies(seed=seed, browsing_history=history, now=_FIXED_NOW) - fbp = next((c for c in cookies if c["name"] == "_fbp"), None) - if fbp: - parts = fbp["value"].split(".") - assert parts[0] == "fb" - assert parts[1].isdigit() - assert parts[2].isdigit() and len(parts[2]) >= 13 # unix ms - assert parts[3].isdigit() - return - raise AssertionError("never got _fbp across 20 seeds — distribution broken") diff --git a/tests/test_service_worker.py b/tests/test_service_worker.py deleted file mode 100644 index d077c99..0000000 --- a/tests/test_service_worker.py +++ /dev/null @@ -1,226 +0,0 @@ -"""Service worker interception regression tests — issue #18 root cause. - -The bug: `juggler/content/NetworkObserver.js:channelIntercepted` called -`interceptedChannel.interceptAfterServiceWorkerResets()` — an IDL method -that upstream Playwright adds via a C++ patch (InterceptedHttpChannel.cpp -+ nsINetworkInterceptController.idl). Our fork was missing those patches -until firefox-6, so the call threw TypeError → C++ NetworkObserver was -left in an inconsistent state → content process disposal manifested as -"page crash" on sites whose service workers fall through to the network -(e.g., id.sky.com). - -These tests inline-serve a service worker via data: URLs / blob URLs -where possible — no external network required. They assert the page -stays alive across SW registration + fetch lifecycle. - -Run: - pytest tests/test_service_worker.py -m e2e -v - -For dev iteration: - INVPW_BINARY_PATH=/path/to/firefox.exe pytest tests/test_service_worker.py -m e2e -v -""" -from __future__ import annotations - -import http.server -import socketserver -import threading - -import pytest - -from invisible_playwright import InvisiblePlaywright - - -# --------------------------------------------------------------------------- -# Local HTTP fixture server — service workers need a real http(s) origin -# (data: and about:blank are opaque-origin, no SW registration possible). -# --------------------------------------------------------------------------- - - -class _SWFixtureHandler(http.server.BaseHTTPRequestHandler): - """Serves a tiny set of routes for SW lifecycle testing.""" - - PAGES = { - "/": (200, "text/html", b""" -sw-host - - - -"""), - "/sw.js": (200, "application/javascript", b""" -self.addEventListener('install', e => self.skipWaiting()); -self.addEventListener('activate', e => e.waitUntil(clients.claim())); -self.addEventListener('fetch', e => { - if (e.request.url.endsWith('/from-sw')) { - e.respondWith(new Response('hello from SW', { - headers: {'content-type': 'text/plain'}, - })); - } - // Fall through for everything else - exercises the - // interceptAfterServiceWorkerResets path that was broken pre-firefox-6. -}); -"""), - "/from-sw": (200, "text/plain", b"network-fallback"), - "/from-network": (200, "text/plain", b"net-only"), - } - - def do_GET(self): - path = self.path.split("?", 1)[0] - if path in self.PAGES: - status, ctype, body = self.PAGES[path] - self.send_response(status) - self.send_header("Content-Type", ctype) - self.send_header("Content-Length", str(len(body))) - # SW requires HTTPS or localhost — we're on localhost so plain http is fine - self.send_header("Service-Worker-Allowed", "/") - self.end_headers() - self.wfile.write(body) - else: - self.send_response(404) - self.end_headers() - - def log_message(self, *args, **kwargs): - pass # silence stdout - - -@pytest.fixture(scope="module") -def fixture_server(): - """Spin up a localhost HTTP server with SW-friendly headers. Yields - the base URL (e.g., 'http://127.0.0.1:54321').""" - httpd = socketserver.TCPServer(("127.0.0.1", 0), _SWFixtureHandler) - port = httpd.server_address[1] - thread = threading.Thread(target=httpd.serve_forever, daemon=True) - thread.start() - try: - yield f"http://127.0.0.1:{port}" - finally: - httpd.shutdown() - httpd.server_close() - - -@pytest.fixture(scope="module") -def page(firefox_binary): - with InvisiblePlaywright( - seed=42, - binary_path=firefox_binary, - headless=True, - ) as browser: - ctx = browser.new_context() - p = ctx.new_page() - yield p - - -# --------------------------------------------------------------------------- -# Regression tests -# --------------------------------------------------------------------------- - - -@pytest.mark.e2e -def test_service_worker_registration_does_not_crash_page(page, fixture_server): - """Navigate to a page that registers a SW. The page must survive the - registration. Pre-firefox-6 this crashed if the SW path hit the missing - `interceptAfterServiceWorkerResets()` IDL method.""" - crashed = {"v": False} - page.on("crash", lambda p: crashed.__setitem__("v", True)) - - page.goto(f"{fixture_server}/", timeout=15_000) - # Wait for SW to register (or fail cleanly) - page.wait_for_function( - "window.__swState !== 'loading'", timeout=10_000 - ) - state = page.evaluate("window.__swState") - assert not crashed["v"], f"page crashed during SW registration (state={state!r})" - # state should be 'registered' or 'failed:...' (Firefox supports SW) - assert state in ("registered",) or state.startswith("failed:"), ( - f"unexpected SW state: {state!r}" - ) - - -@pytest.mark.e2e -def test_page_with_sw_can_navigate_repeatedly(page, fixture_server): - """Once a SW is registered, repeated navigations exercise the - interception path on every request. Pre-firefox-6, this hit the C++ - crash after a few cycles.""" - crashed = {"v": False} - page.on("crash", lambda p: crashed.__setitem__("v", True)) - - page.goto(f"{fixture_server}/", timeout=15_000) - page.wait_for_function("window.__swState !== 'loading'", timeout=10_000) - - # 5 reloads — the SW fetch handler runs each time - for _ in range(5): - page.reload(timeout=15_000) - assert not crashed["v"] - assert page.evaluate("document.title") == "sw-host" - - -@pytest.mark.e2e -def test_fetch_through_sw_returns_sw_synthesized_response(page, fixture_server): - """The SW intercepts `/from-sw` and synthesizes a response without - hitting the network. Verifies the SW fetch path is functional — this - is the exact flow that crashed in id.sky.com.""" - page.goto(f"{fixture_server}/", timeout=15_000) - page.wait_for_function("window.__swState === 'registered'", timeout=10_000) - - # First request to /from-sw routes through the SW - body = page.evaluate("""async (base) => { - const r = await fetch(base + '/from-sw'); - return await r.text(); - }""", fixture_server) - # Either the SW served 'hello from SW' (intercepted) or the network - # served 'network-fallback' (if SW didn't claim yet). Both are OK — - # the regression we test is that it doesn't CRASH. - assert body in ("hello from SW", "network-fallback"), ( - f"unexpected /from-sw response body: {body!r}" - ) - - -@pytest.mark.e2e -def test_sw_fall_through_to_network_does_not_crash(page, fixture_server): - """Request a URL the SW doesn't handle → falls through to network. - This is the `interceptAfterServiceWorkerResets()` code path: the SW - decides not to handle, the channel goes back to network. Without the - C++ patch, this is where the C++ side ended up in an inconsistent - state.""" - crashed = {"v": False} - page.on("crash", lambda p: crashed.__setitem__("v", True)) - - page.goto(f"{fixture_server}/", timeout=15_000) - page.wait_for_function("window.__swState === 'registered'", timeout=10_000) - - # /from-network is NOT intercepted by SW — exercises the fall-through - body = page.evaluate("""async (base) => { - const r = await fetch(base + '/from-network'); - return await r.text(); - }""", fixture_server) - assert body == "net-only" - assert not crashed["v"] - - -@pytest.mark.e2e -def test_sw_unregister_then_register_again(page, fixture_server): - """Unregistering then re-registering exercises lifecycle bookkeeping - in the C++ InterceptedHttpChannel state machine.""" - crashed = {"v": False} - page.on("crash", lambda p: crashed.__setitem__("v", True)) - - page.goto(f"{fixture_server}/", timeout=15_000) - page.wait_for_function("window.__swState === 'registered'", timeout=10_000) - - # Unregister all SWs then register again - result = page.evaluate("""async () => { - const regs = await navigator.serviceWorker.getRegistrations(); - for (const r of regs) await r.unregister(); - const r2 = await navigator.serviceWorker.register('/sw.js'); - return r2.scope; - }""") - assert "/" in result - assert not crashed["v"] diff --git a/tests/test_version.py b/tests/test_version.py deleted file mode 100644 index 7702f7f..0000000 --- a/tests/test_version.py +++ /dev/null @@ -1,103 +0,0 @@ -"""Regression tests for issue #24: CLI version reporting. - -Two distinct symptoms reported by `i43-j`: - 1. `python -m invisible_playwright --version` errored out (only the - `version` subcommand worked). - 2. `python -m invisible_playwright version` printed the literal string - "0.1.0" regardless of the installed version (a stale hardcoded - `__version__` in __init__.py that nobody had remembered to bump). - -These tests pin down both behaviours so the regressions don't sneak back -in via a future copy/paste. -""" -import io -import re -import subprocess -import sys -from contextlib import redirect_stdout - -import pytest - -import invisible_playwright -from invisible_playwright import __version__, cli - - -pytestmark = pytest.mark.unit - - -def test_version_matches_installed_package_metadata(): - """__version__ must come from importlib.metadata, not a hardcoded literal, - so it can never drift from the pyproject.toml `version` field.""" - from importlib.metadata import version as pkg_version - assert __version__ == pkg_version("invisible-playwright") - - -def test_version_is_not_the_stale_010_string(): - """Issue #24 regression: __version__ used to be hardcoded as '0.1.0' - and never updated. If this ever returns to a literal '0.1.0' the - package has been published or shipped with stale metadata.""" - assert __version__ != "0.1.0", ( - "__version__ is the stale hardcoded '0.1.0' string — issue #24 has " - "regressed. Use importlib.metadata to derive it from pyproject.toml." - ) - - -def test_version_subcommand_prints_real_version(): - """`invisible-playwright version` must print the actual installed version, - not the old hardcoded '0.1.0'.""" - buf = io.StringIO() - with redirect_stdout(buf): - rc = cli.main(["version"]) - assert rc == 0 - out = buf.getvalue() - assert f"invisible_playwright {__version__}" in out - assert "0.1.0" not in out or __version__ == "0.1.0" # safety: only allowed if truly 0.1.0 - assert "BINARY_VERSION=" in out - assert "Firefox " in out - - -def test_dash_dash_version_flag_works(): - """Issue #24 reporter: `python -m invisible_playwright --version` used to - error with 'the following arguments are required: cmd' because there was - no top-level --version flag, only the `version` subcommand. Now the - Python convention works too.""" - # argparse's --version action calls sys.exit(0) directly, so use subprocess. - r = subprocess.run( - [sys.executable, "-m", "invisible_playwright", "--version"], - capture_output=True, text=True, timeout=15, - ) - assert r.returncode == 0, f"--version returned {r.returncode}, stderr={r.stderr!r}" - # argparse may emit on stdout or stderr depending on version - combined = r.stdout + r.stderr - assert "invisible_playwright" in combined - assert __version__ in combined - - -def test_no_args_prints_help_not_traceback(): - """`python -m invisible_playwright` with no args should be graceful - (print help, exit non-zero) rather than crashing with a traceback.""" - r = subprocess.run( - [sys.executable, "-m", "invisible_playwright"], - capture_output=True, text=True, timeout=15, - ) - # Either prints help (rc=2) or shows usage. Must NOT contain a traceback. - assert "Traceback" not in (r.stdout + r.stderr) - assert "usage:" in (r.stdout + r.stderr).lower() - - -def test_dash_V_short_flag_works(): - """Alias `-V` for `--version` (Python convention).""" - r = subprocess.run( - [sys.executable, "-m", "invisible_playwright", "-V"], - capture_output=True, text=True, timeout=15, - ) - assert r.returncode == 0 - assert __version__ in (r.stdout + r.stderr) - - -def test_version_matches_semver_shape(): - """Sanity: version should look like a semver (digits.digits.digits) - or a PEP-440 dev marker, not a placeholder string.""" - assert re.match(r"^\d+\.\d+\.\d+", __version__), ( - f"__version__ {__version__!r} doesn't look like a real version" - ) diff --git a/tests/test_webrtc_realness.py b/tests/test_webrtc_realness.py deleted file mode 100644 index 61d487f..0000000 --- a/tests/test_webrtc_realness.py +++ /dev/null @@ -1,490 +0,0 @@ -"""WebRTC realness regression tests. - -Two layers, both runnable on GitHub CI: - -* **unit** (`@pytest.mark.unit`) — pure SDP/candidate assertions against golden - samples. No browser, no proxy, no network. These lock in every rule we found - on 2026-06-06: host must be mDNS ``.local``; the synthetic srflx must carry the - egress IP with a GENUINE nICEr priority (never ``local_pref == 0xFFFF``) and a - stable, distinct foundation; CreepJS's resolver must return the egress, and a - host-only SDP must read as "blocked". They run in the standard ``tests.yml``. - -* **e2e** (`@pytest.mark.e2e`) — launch the patched binary and verify the live - ICE gather. "Being behind a proxy" is faked WITHOUT smartproxy: - - the egress IP is injected via ``STEALTHFOX_WEBRTC_PUBLIC_IP`` (RFC 5737 - TEST-NET, so it never collides with a real IP); - - the "behind a TCP-only SOCKS proxy" condition is reproduced by a tiny - in-process SOCKS5 server that relays TCP CONNECT but refuses UDP ASSOCIATE - (exactly a residential TCP-only proxy → WebRTC's default-route UDP probe - fails → exercises the Fix C fallback). No credentials, no external proxy. - Excluded from the default run; a binary is located via ``STEALTHFOX_E2E_BINARY`` - (or the locally-built tree), else the test skips. -""" -from __future__ import annotations - -import os -import re -import select -import socket -import struct -import threading -from http.server import BaseHTTPRequestHandler, HTTPServer - -import pytest - -# ────────────────────────────────────────────────────────────────────────── -# Pure SDP / ICE-candidate helpers (no I/O) — the heart of the sentinels. -# ────────────────────────────────────────────────────────────────────────── -_CAND = re.compile( - r"candidate:(?P\S+)\s+(?P\d+)\s+(?PUDP|TCP|udp|tcp)\s+" - r"(?P\d+)\s+(?P
\S+)\s+(?P\d+)\s+typ\s+(?P\w+)" - r"(?:.*?raddr\s+(?P\S+)\s+rport\s+(?P\d+))?" -) - - -def parse_candidate(line): - """Parse one ``a=candidate:`` / ``candidate:`` line into a dict (or None).""" - m = _CAND.search(line) - if not m: - return None - d = m.groupdict() - d["component"] = int(d["component"]) - d["priority"] = int(d["priority"]) - d["port"] = int(d["port"]) - d["proto"] = d["proto"].upper() - if d["rport"] is not None: - d["rport"] = int(d["rport"]) - return d - - -def decode_priority(prio): - """Split a candidate priority into nICEr's fields (RFC 5245 layout that - nICEr emits: type<<24 | iface<<16 | dir<<13 | stun<<8 | (256-component)).""" - return { - "type_pref": (prio >> 24) & 0xFF, - "iface_pref": (prio >> 16) & 0xFF, - "local_pref": (prio >> 8) & 0xFFFF, - "direction": (prio >> 13) & 0x7, - "stun_priority": (prio >> 8) & 0x1F, - "component": 256 - (prio & 0xFF), - } - - -def is_mdns(addr): - return bool(addr) and str(addr).endswith(".local") - - -def candidates(sdp_or_lines): - if isinstance(sdp_or_lines, str): - lines = re.findall(r"(?:a=)?candidate:[^\r\n]*", sdp_or_lines) - else: - lines = list(sdp_or_lines) - return [c for c in (parse_candidate(l) for l in lines) if c] - - -def host_candidates(cands): - return [c for c in cands if c["typ"] == "host"] - - -def srflx_candidates(cands): - return [c for c in cands if c["typ"] == "srflx"] - - -def host_is_mdns(cands): - """Every host candidate must be a ``.local`` mDNS name, never a raw - LAN IP (the §9.4 leak form that fails BrowserLeaks).""" - hosts = host_candidates(cands) - return bool(hosts) and all(is_mdns(c["address"]) for c in hosts) - - -def srflx_realness(cand, expected_ip=None): - """Return (ok, reasons) for whether ``cand`` looks like a GENUINE nICEr UDP - server-reflexive candidate. Encodes the 2026-06-06 findings.""" - reasons = [] - if cand["typ"] != "srflx": - reasons.append("not a srflx candidate") - return False, reasons - if expected_ip is not None and cand["address"] != expected_ip: - reasons.append(f"address {cand['address']} != expected {expected_ip}") - p = decode_priority(cand["priority"]) - if p["type_pref"] != 100: - reasons.append(f"type_pref {p['type_pref']} != 100 (SRV_RFLX)") - if p["local_pref"] == 0xFFFF: - reasons.append("local_pref == 0xFFFF — impossible nICEr value (the old hardcoded tell)") - elif not (0x7000 <= p["local_pref"] < 0x8000): - reasons.append(f"local_pref {p['local_pref']} outside the genuine ~0x7E00-0x7FFF band") - if not (16 <= p["stun_priority"] <= 31): - reasons.append(f"stun_priority {p['stun_priority']} implausible (expect 31-server_id)") - if cand.get("raddr") not in (None, "0.0.0.0"): - reasons.append(f"raddr {cand['raddr']} not redacted to 0.0.0.0") - return (not reasons), reasons - - -def creep_get_ipaddress(sdp): - """Faithful port of CreepJS's getIPAddress(sdp): connection line first, then - the first candidate IP; '0.0.0.0' counts as blocked. Returns None if blocked - — i.e. exactly what makes CreepJS render 'stun connection: blocked'.""" - blocked = "0.0.0.0" - conn = (re.findall(r"c=IN\s.+\s", sdp) or [""])[0].strip().split(" ") - conn_ip = conn[2] if len(conn) > 2 else "" - if conn_ip and conn_ip != blocked: - return conn_ip - m = re.search(r"(udp|tcp)\s(?:\d|\w)+\s((?:\d|\w|\.|:)+)(?=\s)", sdp, re.I) - ip = m.group(2) if m else None - return ip if (ip and ip != blocked) else None - - -# ────────────────────────────────────────────────────────────────────────── -# Golden samples — real priority/foundation values, TEST-NET IPs (RFC 5737) -# so no real address is ever committed (feedback_pre_push_privacy_check). -# ────────────────────────────────────────────────────────────────────────── -HOST_MDNS = "candidate:0 1 UDP 2122252543 1460e928-16b3-4c66-80ad-04abcdef0000.local 54551 typ host" -HOST_RAW_IP = "candidate:0 1 UDP 2122252543 192.168.1.20 54551 typ host" # §9.4 leak form -VANILLA_SRFLX = "candidate:1 1 UDP 1685987327 203.0.113.50 3755 typ srflx raddr 0.0.0.0 rport 0" -OURS_SRFLX = "candidate:1 1 UDP 1686052863 203.0.113.7 58555 typ srflx raddr 0.0.0.0 rport 0" -# Pre-fix injection: local_pref hardcoded to 0xFFFF (priority 1694498815). The tell. -OLD_BAD_SRFLX = "candidate:2 1 UDP 1694498815 203.0.113.7 58555 typ srflx raddr 0.0.0.0 rport 0" - -SDP_GOOD = ( - "v=0\r\nc=IN IP4 0.0.0.0\r\n" - f"a={HOST_MDNS}\r\na={OURS_SRFLX}\r\n" -) -SDP_BLOCKED = "v=0\r\nc=IN IP4 0.0.0.0\r\n" f"a={HOST_MDNS}\r\n" # host-only, no srflx - - -# ────────────────────────────────────────────────────────────────────────── -# UNIT sentinels (run on GitHub CI) -# ────────────────────────────────────────────────────────────────────────── -@pytest.mark.unit -def test_parse_and_decode_basics(): - c = parse_candidate(OURS_SRFLX) - assert c["typ"] == "srflx" and c["proto"] == "UDP" - assert c["address"] == "203.0.113.7" and c["raddr"] == "0.0.0.0" and c["rport"] == 0 - p = decode_priority(c["priority"]) - assert p["type_pref"] == 100 and p["stun_priority"] == 31 and p["component"] == 1 - - -@pytest.mark.unit -def test_genuine_srflx_passes(): - for line in (VANILLA_SRFLX, OURS_SRFLX): - ok, reasons = srflx_realness(parse_candidate(line), expected_ip=parse_candidate(line)["address"]) - assert ok, reasons - - -@pytest.mark.unit -def test_old_0xffff_srflx_is_rejected(): - """Fix A sentinel: local_pref == 0xFFFF must be flagged as fake.""" - ok, reasons = srflx_realness(parse_candidate(OLD_BAD_SRFLX)) - assert not ok - assert any("0xFFFF" in r for r in reasons), reasons - - -@pytest.mark.unit -def test_host_must_be_mdns_not_raw_ip(): - """§9.4 sentinel: raw-IP host candidate is a leak; .local is required.""" - assert host_is_mdns(candidates([HOST_MDNS])) is True - assert host_is_mdns(candidates([HOST_RAW_IP])) is False - - -@pytest.mark.unit -def test_srflx_foundation_distinct_from_host(): - """Fix B sentinel: srflx foundation must differ from the host foundations.""" - cands = candidates([HOST_MDNS, OURS_SRFLX]) - host_fnds = {c["foundation"] for c in host_candidates(cands)} - srflx_fnds = {c["foundation"] for c in srflx_candidates(cands)} - assert srflx_fnds and srflx_fnds.isdisjoint(host_fnds) - - -@pytest.mark.unit -def test_creep_resolver_returns_egress_when_srflx_present(): - assert creep_get_ipaddress(SDP_GOOD) == "203.0.113.7" - - -@pytest.mark.unit -def test_creep_resolver_reports_blocked_for_host_only(): - """The exact false-green we shipped: host-only (.local) SDP → no public IP - → CreepJS shows 'blocked'. The resolver must return None here.""" - assert creep_get_ipaddress(SDP_BLOCKED) is None - - -@pytest.mark.unit -def test_mdns_host_is_invisible_to_creep_resolver(): - """A .local host must NOT be mis-read as an IP (the hyphen in the UUID is - what makes CreepJS skip it and fall through to the srflx).""" - assert creep_get_ipaddress("v=0\r\nc=IN IP4 0.0.0.0\r\n" f"a={HOST_MDNS}\r\n") is None - - -# ────────────────────────────────────────────────────────────────────────── -# SHIPPED-BASELINE guard — the cheap unit test that would have caught the -# 2026-06-10 gap (baseline obfuscate=False, dead disableIPv6, orphan prefs). -# These lock the shipped wrapper config to the manually-validated one so a -# future edit / merge can't silently un-ship it. Run in tests.yml. -# ────────────────────────────────────────────────────────────────────────── -from invisible_playwright._fpforge import generate_profile # noqa: E402 -from invisible_playwright.prefs import translate_profile_to_prefs # noqa: E402 - - -@pytest.mark.unit -def test_shipped_webrtc_baseline_is_the_validated_config(): - prefs = translate_profile_to_prefs(generate_profile(seed=42)) - # host candidate must be mDNS .local like vanilla Firefox (manually - # validated on BrowserLeaks/CreepJS through a residential proxy) — not a - # raw LAN IP. - assert prefs["media.peerconnection.ice.obfuscate_host_addresses"] is True - # IPv6 dropped via OUR live filter pref; the native pref is dead on FF150 - # and must not be relied upon (or re-introduced as if it worked). - assert prefs["zoom.stealth.webrtc.disable_ipv6"] is True - assert "media.peerconnection.ice.disableIPv6" not in prefs - # peerconnection stays ON (a disabled WebRTC is itself a tell). - assert prefs["media.peerconnection.enabled"] is True - - -@pytest.mark.unit -def test_no_orphan_prefs_in_baseline(): - """zoom.stealth.timezone / zoom.stealth.seed are read by NO C++ — they must - not be written (juggler.timezone.override + zoom.stealth.fpp.hw_seed are the - real ones). Guards against re-introducing a pref the binary ignores.""" - prefs = translate_profile_to_prefs(generate_profile(seed=42), timezone="America/Chicago") - assert "zoom.stealth.timezone" not in prefs - assert "zoom.stealth.seed" not in prefs - assert prefs["juggler.timezone.override"] == "America/Chicago" - assert "zoom.stealth.fpp.hw_seed" in prefs - - -# ────────────────────────────────────────────────────────────────────────── -# Fake-proxy infrastructure for e2e: a tiny TCP-only SOCKS5 server. -# ────────────────────────────────────────────────────────────────────────── -class _Socks5TcpOnly: - """Minimal SOCKS5: no-auth, CONNECT (TCP) relayed, UDP ASSOCIATE refused. - - Reproduces a residential TCP-only proxy: pages load over TCP, but WebRTC's - UDP path is dead — which (for a no-camera page in default_address_only mode) - is exactly what made the default-route probe fail and ICE return zero - candidates before Fix C. - """ - - def __init__(self): - self._srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - self._srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self._srv.bind(("127.0.0.1", 0)) - self._srv.listen(16) - self.port = self._srv.getsockname()[1] - self.udp_associate_attempts = 0 - self._stop = False - self._t = threading.Thread(target=self._serve, daemon=True) - self._t.start() - - def _serve(self): - while not self._stop: - try: - conn, _ = self._srv.accept() - except OSError: - break - threading.Thread(target=self._handle, args=(conn,), daemon=True).start() - - def _recv_exact(self, sock, n): - buf = b"" - while len(buf) < n: - chunk = sock.recv(n - len(buf)) - if not chunk: - return None - buf += chunk - return buf - - def _handle(self, conn): - try: - head = self._recv_exact(conn, 2) - if not head or head[0] != 0x05: - conn.close() - return - nmethods = head[1] - self._recv_exact(conn, nmethods) - conn.sendall(b"\x05\x00") # no-auth - req = self._recv_exact(conn, 4) - if not req: - conn.close() - return - ver, cmd, _, atyp = req - if atyp == 0x01: - addr = socket.inet_ntoa(self._recv_exact(conn, 4)) - elif atyp == 0x03: - ln = self._recv_exact(conn, 1)[0] - addr = self._recv_exact(conn, ln).decode("ascii", "ignore") - elif atyp == 0x04: - addr = socket.inet_ntop(socket.AF_INET6, self._recv_exact(conn, 16)) - else: - conn.close() - return - port = struct.unpack("!H", self._recv_exact(conn, 2))[0] - if cmd != 0x01: # not CONNECT (e.g. UDP ASSOCIATE) → refuse - self.udp_associate_attempts += 1 - conn.sendall(b"\x05\x07\x00\x01\x00\x00\x00\x00\x00\x00") # cmd not supported - conn.close() - return - try: - upstream = socket.create_connection((addr, port), timeout=15) - except OSError: - conn.sendall(b"\x05\x04\x00\x01\x00\x00\x00\x00\x00\x00") # host unreachable - conn.close() - return - conn.sendall(b"\x05\x00\x00\x01\x00\x00\x00\x00\x00\x00") # success - self._relay(conn, upstream) - except Exception: - try: - conn.close() - except Exception: - pass - - def _relay(self, a, b): - try: - while True: - r, _, _ = select.select([a, b], [], [], 30) - if not r: - break - for s in r: - data = s.recv(65536) - if not data: - return - (b if s is a else a).sendall(data) - finally: - for s in (a, b): - try: - s.close() - except Exception: - pass - - def close(self): - self._stop = True - try: - self._srv.close() - except Exception: - pass - - -# Same per-event probe CreepJS runs (kept tiny; raw string = one escape level). -_PROBE_JS = r"""async () => { - const pc = new RTCPeerConnection({iceCandidatePoolSize:1, iceServers:[{urls:[ - 'stun:stun4.l.google.com:19302','stun:stun3.l.google.com:19302']}]}); - pc.createDataChannel(''); - const cands = []; - pc.addEventListener('icecandidate', e => { if (e.candidate && e.candidate.candidate) cands.push(e.candidate.candidate); }); - await pc.setLocalDescription(await pc.createOffer({offerToReceiveAudio:1, offerToReceiveVideo:1})); - await new Promise(r => setTimeout(r, 3500)); - const sdp = (pc.localDescription && pc.localDescription.sdp) || ''; - try { pc.close(); } catch(e) {} - return { candidates: cands, sdp }; -}""" - -_FAKE_EGRESS = "203.0.113.7" # RFC 5737 TEST-NET-3 - - -def _e2e_binary(): - # Honor both env vars so the whole e2e suite targets one binary from a single - # setting (INVPW_BINARY_PATH is what conftest's firefox_binary uses). - cand = os.environ.get("STEALTHFOX_E2E_BINARY") or os.environ.get("INVPW_BINARY_PATH") - if cand and os.path.exists(cand): - return cand - built = r"C:\ff\source\obj-x86_64-pc-windows-msvc\dist\bin\firefox.exe" - if os.path.exists(built): - return built - return None - - -@pytest.fixture -def socks5_tcp_only(): - srv = _Socks5TcpOnly() - yield srv - srv.close() - - -@pytest.fixture -def local_https_page(): - """A trivial localhost page (used by the no-proxy srflx test).""" - class H(BaseHTTPRequestHandler): - def do_GET(self): - self.send_response(200) - self.send_header("Content-Type", "text/html") - self.end_headers() - self.wfile.write(b"wrtc") - - def log_message(self, *a): - pass - - httpd = HTTPServer(("127.0.0.1", 0), H) - threading.Thread(target=httpd.serve_forever, daemon=True).start() - yield f"http://127.0.0.1:{httpd.server_address[1]}/" - httpd.shutdown() - - -def _launch(**extra): - from invisible_playwright import InvisiblePlaywright - - kw = {"headless": True, - # Fixed zone so the wrapper does NOT run timezone="auto" egress - # discovery through the (fake) proxy — irrelevant here, we inject the - # egress IP directly and want the launch deterministic/offline. - "timezone": "America/New_York", - "extra_prefs": {"media.peerconnection.ice.obfuscate_host_addresses": True}} - kw.update(extra) - return InvisiblePlaywright(**kw) - - -@pytest.mark.e2e -def test_srflx_is_real_and_resolvable(local_https_page): - """No proxy needed: the egress is faked via the env. Asserts the live srflx - is genuine (Fix A/B) and that CreepJS's resolver returns it (not blocked).""" - binary = _e2e_binary() - if not binary: - pytest.skip("no patched binary (set STEALTHFOX_E2E_BINARY)") - os.environ["STEALTHFOX_WEBRTC_PUBLIC_IP"] = _FAKE_EGRESS - os.environ["STEALTHFOX_WEBRTC_DISABLE_IPV6"] = "1" - with _launch(binary_path=binary) as browser: - page = browser.new_context().new_page() - page.goto(local_https_page, wait_until="domcontentloaded", timeout=60000) - res = page.evaluate(_PROBE_JS) - cands = candidates(res["candidates"]) - assert cands, "ICE produced ZERO candidates (blocked)" - assert host_is_mdns(cands), [c["address"] for c in host_candidates(cands)] - srflx = [c for c in srflx_candidates(cands) if c["address"] == _FAKE_EGRESS] - assert srflx, f"no synthetic srflx with {_FAKE_EGRESS}: {res['candidates']}" - ok, reasons = srflx_realness(srflx[0], expected_ip=_FAKE_EGRESS) - assert ok, reasons - # Two srflx for the same base must share ONE stable foundation (Fix B). - assert len({c["foundation"] for c in srflx}) == 1 - assert creep_get_ipaddress(res["sdp"]) == _FAKE_EGRESS - - -@pytest.mark.e2e -def test_not_blocked_behind_tcp_only_socks(socks5_tcp_only): - """Fix C sentinel: behind a TCP-only SOCKS proxy on a remote origin, ICE - must still complete (host .local + synthetic srflx), not return zero - candidates. Without Fix C this page is fully 'blocked'.""" - binary = _e2e_binary() - if not binary: - pytest.skip("no patched binary (set STEALTHFOX_E2E_BINARY)") - os.environ["STEALTHFOX_WEBRTC_PUBLIC_IP"] = _FAKE_EGRESS - os.environ["STEALTHFOX_WEBRTC_DISABLE_IPV6"] = "1" - proxy = {"server": f"socks5://127.0.0.1:{socks5_tcp_only.port}"} - try: - with _launch(binary_path=binary, proxy=proxy) as browser: - page = browser.new_context().new_page() - # remote origin loaded THROUGH the local SOCKS proxy (not localhost, - # so no proxy-bypass) → WebRTC proxy config active → Fix C path. - page.goto("https://example.com/", wait_until="domcontentloaded", timeout=70000) - res = page.evaluate(_PROBE_JS) - except Exception as exc: # network/proxy unavailable in this environment - pytest.skip(f"proxy/network path unavailable: {exc!r}") - cands = candidates(res["candidates"]) - # Hard regression check: ZERO candidates means WebRTC is fully blocked behind - # the SOCKS proxy — that's the Fix C regression this sentinel exists to catch. - assert cands, "behind SOCKS the gather returned ZERO candidates — Fix C regressed (blocked)" - assert host_is_mdns(cands) - # The synthetic srflx (= fake egress) needs the remote origin to load FULLY - # through the proxy so the WebRTC proxy config engages. That path is - # environment-sensitive (it doesn't always engage on a datacenter CI box even - # though host candidates gather), so treat a missing srflx as a skip, not a - # failure — the local run validates it where the path is real. - if not any(c["address"] == _FAKE_EGRESS for c in srflx_candidates(cands)): - pytest.skip("synthetic srflx not engaged in this environment " - "(needs the remote origin fully through the proxy); validated locally") - assert creep_get_ipaddress(res["sdp"]) == _FAKE_EGRESS diff --git a/tests/unit/test_config_public.py b/tests/unit/test_config_public.py deleted file mode 100644 index f589271..0000000 --- a/tests/unit/test_config_public.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Unit tests for the public ``config`` helpers.""" - -import pytest - -from invisible_playwright import ( - ensure_binary, - get_default_args, - get_default_stealth_prefs, -) -from invisible_playwright.config import get_default_stealth_prefs as _direct - - -pytestmark = pytest.mark.unit - - -def test_get_default_args_is_empty_list(): - """Currently no baseline CLI args, but must return a list (mutable, fresh each call).""" - args = get_default_args() - assert args == [] - assert isinstance(args, list) - args.append("--foo") - # next call must return a fresh empty list, not the mutated one - assert get_default_args() == [] - - -def test_get_default_stealth_prefs_random_seed_returns_dict(): - """No seed -> fresh random fingerprint, dict has expected stealth keys.""" - prefs = get_default_stealth_prefs() - assert isinstance(prefs, dict) - assert len(prefs) > 0 - # humanize toggle is always set explicitly - assert "stealthfox.humanize" in prefs - assert prefs["stealthfox.humanize"] is True - - -def test_get_default_stealth_prefs_seed_is_deterministic(): - """Same seed -> byte-identical prefs across calls.""" - a = get_default_stealth_prefs(seed=42) - b = get_default_stealth_prefs(seed=42) - assert a == b - - -def test_get_default_stealth_prefs_different_seeds_differ(): - """Different seeds -> different prefs.""" - a = get_default_stealth_prefs(seed=1) - b = get_default_stealth_prefs(seed=2) - assert a != b - - -def test_humanize_false_disables_prefs(): - """humanize=False removes the maxTime knob and flips the toggle to False.""" - prefs = get_default_stealth_prefs(seed=42, humanize=False) - assert prefs["stealthfox.humanize"] is False - assert "stealthfox.humanize.maxTime" not in prefs - - -def test_humanize_default_sets_max_time_1_5(): - """humanize=True -> default maxTime is 1.5s, stored as string.""" - prefs = get_default_stealth_prefs(seed=42, humanize=True) - assert prefs["stealthfox.humanize"] is True - assert prefs["stealthfox.humanize.maxTime"] == "1.5" - - -def test_humanize_float_overrides_max_time(): - """Float for humanize is the explicit cap in seconds.""" - prefs = get_default_stealth_prefs(seed=42, humanize=3.0) - assert prefs["stealthfox.humanize"] is True - assert prefs["stealthfox.humanize.maxTime"] == "3.0" - - -def test_extra_prefs_overlay_takes_precedence(): - """extra_prefs overlay LAST overrides any baseline value.""" - prefs = get_default_stealth_prefs( - seed=42, extra_prefs={"some.custom.pref": 999} - ) - assert prefs["some.custom.pref"] == 999 - - -def test_extra_prefs_can_override_baseline(): - """A key in extra_prefs that also exists in baseline gets overridden.""" - baseline = get_default_stealth_prefs(seed=42) - a_baseline_key = next(iter(baseline.keys())) - overridden = get_default_stealth_prefs( - seed=42, extra_prefs={a_baseline_key: "OVERRIDDEN_SENTINEL"} - ) - assert overridden[a_baseline_key] == "OVERRIDDEN_SENTINEL" - - -def test_locale_argument_changes_prefs(): - """Different locales produce different prefs (Accept-Language affected).""" - en = get_default_stealth_prefs(seed=42, locale="en-US") - it = get_default_stealth_prefs(seed=42, locale="it-IT") - assert en != it - - -def test_timezone_argument_changes_prefs(): - """Different timezones produce different prefs.""" - ny = get_default_stealth_prefs(seed=42, timezone="America/New_York") - rome = get_default_stealth_prefs(seed=42, timezone="Europe/Rome") - assert ny != rome - - -def test_pin_argument_forces_specific_fields(): - """Pin forces a specific field while the rest stays seed-derived.""" - plain = get_default_stealth_prefs(seed=42) - pinned = get_default_stealth_prefs( - seed=42, pin={"hardware.concurrency": 999} - ) - # something in the dict must differ vs the plain seed=42 build - assert plain != pinned - - -def test_public_import_matches_direct_import(): - """Top-level re-export and direct module import return identical output.""" - a = get_default_stealth_prefs(seed=42) - b = _direct(seed=42) - assert a == b - - -def test_ensure_binary_is_callable_via_public_namespace(): - """ensure_binary is re-exported and stays callable from the package root.""" - # We don't invoke it (would trigger a network download in CI) — just - # verify the public attribute is the same callable as the underlying. - from invisible_playwright.download import ensure_binary as _direct_eb - assert ensure_binary is _direct_eb diff --git a/tests/vendor/README.md b/tests/vendor/README.md deleted file mode 100644 index b252d8b..0000000 --- a/tests/vendor/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Vendored detection libraries (test-only) - -These are upstream, unmodified, MIT-licensed browser-fingerprinting / bot-detection -libraries, vendored so the detector e2e tests run **hermetically and identically** -on a dev box and on a GitHub runner (no external CDN at test time — Firefox -tracking-protection blocks the openfpcdn.io CDN anyway, and we want CI offline). - -They are served from a localhost HTTP server and loaded into the patched Firefox; -the tests assert the REAL detectors don't flag the stealth build (BotD: `bot===false`; -fpscanner: engine-agnostic rules clean; CreepJS: `headlessRating===0` + no JS-proxy -tells) and that the fingerprint is stable (FingerprintJS: same `visitorId` across -launches). CreepJS runs fully offline — the tests abort every non-loopback request, -so its optional crowd-comparison POST never fires and the verdict is computed locally. - -| File | Package | Version | Source | License | -|---|---|---|---|---| -| `botd-2.0.0.esm.js` | `@fingerprintjs/botd` | 2.0.0 | https://cdn.jsdelivr.net/npm/@fingerprintjs/botd@2.0.0/dist/botd.esm.js | MIT | -| `fingerprintjs-5.2.0.umd.min.js` | `@fingerprintjs/fingerprintjs` | 5.2.0 | https://cdn.jsdelivr.net/npm/@fingerprintjs/fingerprintjs@5.2.0/dist/fp.umd.min.js | MIT | -| `fpscanner-1.0.6.es.js` | `fpscanner` | 1.0.6 | https://cdn.jsdelivr.net/npm/fpscanner@1.0.6/dist/fpScanner.es.js | MIT | -| `creepjs-10aa672.js` | `abrahamjuliot/creepjs` | git `10aa6724` | https://raw.githubusercontent.com/abrahamjuliot/creepjs/10aa6724cd33a1015db1574211890518cd04f0cc/docs/creep.js | MIT | - -All MIT (FingerprintJS Inc. / Antoine Vastel / Abraham Juliot). To update: download -the pinned dist (jsdelivr for npm packages, raw.githubusercontent for CreepJS at a -commit SHA), drop it here, and bump the version in the filename + this table. diff --git a/tests/vendor/botd-2.0.0.esm.js b/tests/vendor/botd-2.0.0.esm.js deleted file mode 100644 index 3064a78..0000000 --- a/tests/vendor/botd-2.0.0.esm.js +++ /dev/null @@ -1,811 +0,0 @@ -/** - * Fingerprint BotD v2.0.0 - Copyright (c) FingerprintJS, Inc, 2025 (https://fingerprint.com) - * Licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) license. - */ - -var version = "2.0.0"; - -/** - * Enum for types of bots. - * Specific types of bots come first, followed by automation technologies. - * - * @readonly - * @enum {string} - */ -const BotKind = { - // Object is used instead of Typescript enum to avoid emitting IIFE which might be affected by further tree-shaking. - // See example of compiled enums https://stackoverflow.com/q/47363996) - Awesomium: 'awesomium', - Cef: 'cef', - CefSharp: 'cefsharp', - CoachJS: 'coachjs', - Electron: 'electron', - FMiner: 'fminer', - Geb: 'geb', - NightmareJS: 'nightmarejs', - Phantomas: 'phantomas', - PhantomJS: 'phantomjs', - Rhino: 'rhino', - Selenium: 'selenium', - Sequentum: 'sequentum', - SlimerJS: 'slimerjs', - WebDriverIO: 'webdriverio', - WebDriver: 'webdriver', - HeadlessChrome: 'headless_chrome', - Unknown: 'unknown', -}; -/** - * Bot detection error. - */ -class BotdError extends Error { - /** - * Creates a new BotdError. - * - * @class - */ - constructor(state, message) { - super(message); - this.state = state; - this.name = 'BotdError'; - Object.setPrototypeOf(this, BotdError.prototype); - } -} - -function detect(components, detectors) { - const detections = {}; - let finalDetection = { - bot: false, - }; - for (const detectorName in detectors) { - const detector = detectors[detectorName]; - const detectorRes = detector(components); - let detection = { bot: false }; - if (typeof detectorRes === 'string') { - detection = { bot: true, botKind: detectorRes }; - } - else if (detectorRes) { - detection = { bot: true, botKind: BotKind.Unknown }; - } - detections[detectorName] = detection; - if (detection.bot) { - finalDetection = detection; - } - } - return [detections, finalDetection]; -} -async function collect(sources) { - const components = {}; - const sourcesKeys = Object.keys(sources); - await Promise.all(sourcesKeys.map(async (sourceKey) => { - const res = sources[sourceKey]; - try { - components[sourceKey] = { - value: await res(), - state: 0 /* State.Success */, - }; - } - catch (error) { - if (error instanceof BotdError) { - components[sourceKey] = { - state: error.state, - error: `${error.name}: ${error.message}`, - }; - } - else { - components[sourceKey] = { - state: -3 /* State.UnexpectedBehaviour */, - error: error instanceof Error ? `${error.name}: ${error.message}` : String(error), - }; - } - } - })); - return components; -} - -function detectAppVersion({ appVersion }) { - if (appVersion.state !== 0 /* State.Success */) - return false; - if (/headless/i.test(appVersion.value)) - return BotKind.HeadlessChrome; - if (/electron/i.test(appVersion.value)) - return BotKind.Electron; - if (/slimerjs/i.test(appVersion.value)) - return BotKind.SlimerJS; -} - -function arrayIncludes(arr, value) { - return arr.indexOf(value) !== -1; -} -function strIncludes(str, value) { - return str.indexOf(value) !== -1; -} -function arrayFind(array, callback) { - if ('find' in array) - return array.find(callback); - for (let i = 0; i < array.length; i++) { - if (callback(array[i], i, array)) - return array[i]; - } - return undefined; -} - -function getObjectProps(obj) { - return Object.getOwnPropertyNames(obj); -} -function includes(arr, ...keys) { - for (const key of keys) { - if (typeof key === 'string') { - if (arrayIncludes(arr, key)) - return true; - } - else { - const match = arrayFind(arr, (value) => key.test(value)); - if (match != null) - return true; - } - } - return false; -} -function countTruthy(values) { - return values.reduce((sum, value) => sum + (value ? 1 : 0), 0); -} - -function detectDocumentAttributes({ documentElementKeys }) { - if (documentElementKeys.state !== 0 /* State.Success */) - return false; - if (includes(documentElementKeys.value, 'selenium', 'webdriver', 'driver')) { - return BotKind.Selenium; - } -} - -function detectErrorTrace({ errorTrace }) { - if (errorTrace.state !== 0 /* State.Success */) - return false; - if (/PhantomJS/i.test(errorTrace.value)) - return BotKind.PhantomJS; -} - -function detectEvalLengthInconsistency({ evalLength, browserKind, browserEngineKind, }) { - if (evalLength.state !== 0 /* State.Success */ || - browserKind.state !== 0 /* State.Success */ || - browserEngineKind.state !== 0 /* State.Success */) - return; - const length = evalLength.value; - if (browserEngineKind.value === "unknown" /* BrowserEngineKind.Unknown */) - return false; - return ((length === 37 && !arrayIncludes(["webkit" /* BrowserEngineKind.Webkit */, "gecko" /* BrowserEngineKind.Gecko */], browserEngineKind.value)) || - (length === 39 && !arrayIncludes(["internet_explorer" /* BrowserKind.IE */], browserKind.value)) || - (length === 33 && !arrayIncludes(["chromium" /* BrowserEngineKind.Chromium */], browserEngineKind.value))); -} - -function detectFunctionBind({ functionBind }) { - if (functionBind.state === -2 /* State.NotFunction */) - return BotKind.PhantomJS; -} - -function detectLanguagesLengthInconsistency({ languages }) { - if (languages.state === 0 /* State.Success */ && languages.value.length === 0) { - return BotKind.HeadlessChrome; - } -} - -function detectMimeTypesConsistent({ mimeTypesConsistent }) { - if (mimeTypesConsistent.state === 0 /* State.Success */ && !mimeTypesConsistent.value) { - return BotKind.Unknown; - } -} - -function detectNotificationPermissions({ notificationPermissions, browserKind, }) { - if (browserKind.state !== 0 /* State.Success */ || browserKind.value !== "chrome" /* BrowserKind.Chrome */) - return false; - if (notificationPermissions.state === 0 /* State.Success */ && notificationPermissions.value) { - return BotKind.HeadlessChrome; - } -} - -function detectPluginsArray({ pluginsArray }) { - if (pluginsArray.state === 0 /* State.Success */ && !pluginsArray.value) - return BotKind.HeadlessChrome; -} - -function detectPluginsLengthInconsistency({ pluginsLength, android, browserKind, browserEngineKind, }) { - if (pluginsLength.state !== 0 /* State.Success */ || - android.state !== 0 /* State.Success */ || - browserKind.state !== 0 /* State.Success */ || - browserEngineKind.state !== 0 /* State.Success */) - return; - if (browserKind.value !== "chrome" /* BrowserKind.Chrome */ || - android.value || - browserEngineKind.value !== "chromium" /* BrowserEngineKind.Chromium */) - return; - if (pluginsLength.value === 0) - return BotKind.HeadlessChrome; -} - -function detectProcess({ process }) { - var _a; - if (process.state !== 0 /* State.Success */) - return false; - if (process.value.type === 'renderer' || ((_a = process.value.versions) === null || _a === void 0 ? void 0 : _a.electron) != null) - return BotKind.Electron; -} - -function detectProductSub({ productSub, browserKind }) { - if (productSub.state !== 0 /* State.Success */ || browserKind.state !== 0 /* State.Success */) - return false; - if ((browserKind.value === "chrome" /* BrowserKind.Chrome */ || - browserKind.value === "safari" /* BrowserKind.Safari */ || - browserKind.value === "opera" /* BrowserKind.Opera */ || - browserKind.value === "wechat" /* BrowserKind.WeChat */) && - productSub.value !== '20030107') - return BotKind.Unknown; -} - -function detectUserAgent({ userAgent }) { - if (userAgent.state !== 0 /* State.Success */) - return false; - if (/PhantomJS/i.test(userAgent.value)) - return BotKind.PhantomJS; - if (/Headless/i.test(userAgent.value)) - return BotKind.HeadlessChrome; - if (/Electron/i.test(userAgent.value)) - return BotKind.Electron; - if (/slimerjs/i.test(userAgent.value)) - return BotKind.SlimerJS; -} - -function detectWebDriver({ webDriver }) { - if (webDriver.state === 0 /* State.Success */ && webDriver.value) - return BotKind.HeadlessChrome; -} - -function detectWebGL({ webGL }) { - if (webGL.state === 0 /* State.Success */) { - const { vendor, renderer } = webGL.value; - if (vendor == 'Brian Paul' && renderer == 'Mesa OffScreen') { - return BotKind.HeadlessChrome; - } - } -} - -function detectWindowExternal({ windowExternal }) { - if (windowExternal.state !== 0 /* State.Success */) - return false; - if (/Sequentum/i.test(windowExternal.value)) - return BotKind.Sequentum; -} - -function detectWindowSize({ windowSize, documentFocus }) { - if (windowSize.state !== 0 /* State.Success */ || documentFocus.state !== 0 /* State.Success */) - return false; - const { outerWidth, outerHeight } = windowSize.value; - // When a page is opened in a new tab without focusing it right away, the window outer size is 0x0 - if (!documentFocus.value) - return; - if (outerWidth === 0 && outerHeight === 0) - return BotKind.HeadlessChrome; -} - -function detectDistinctiveProperties({ distinctiveProps }) { - if (distinctiveProps.state !== 0 /* State.Success */) - return false; - const value = distinctiveProps.value; - let bot; - for (bot in value) - if (value[bot]) - return bot; -} - -// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types -const detectors = { - detectAppVersion, - detectDocumentAttributes, - detectErrorTrace, - detectEvalLengthInconsistency, - detectFunctionBind, - detectLanguagesLengthInconsistency, - detectNotificationPermissions, - detectPluginsArray, - detectPluginsLengthInconsistency, - detectProcess, - detectUserAgent, - detectWebDriver, - detectWebGL, - detectWindowExternal, - detectWindowSize, - detectMimeTypesConsistent, - detectProductSub, - detectDistinctiveProperties, -}; - -function getAppVersion() { - const appVersion = navigator.appVersion; - if (appVersion == undefined) { - throw new BotdError(-1 /* State.Undefined */, 'navigator.appVersion is undefined'); - } - return appVersion; -} - -function getDocumentElementKeys() { - if (document.documentElement === undefined) { - throw new BotdError(-1 /* State.Undefined */, 'document.documentElement is undefined'); - } - const { documentElement } = document; - if (typeof documentElement.getAttributeNames !== 'function') { - throw new BotdError(-2 /* State.NotFunction */, 'document.documentElement.getAttributeNames is not a function'); - } - return documentElement.getAttributeNames(); -} - -function getErrorTrace() { - try { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - null[0](); - } - catch (error) { - if (error instanceof Error && error['stack'] != null) { - return error.stack.toString(); - } - } - throw new BotdError(-3 /* State.UnexpectedBehaviour */, 'errorTrace signal unexpected behaviour'); -} - -function getEvalLength() { - return eval.toString().length; -} - -function getFunctionBind() { - if (Function.prototype.bind === undefined) { - throw new BotdError(-2 /* State.NotFunction */, 'Function.prototype.bind is undefined'); - } - return Function.prototype.bind.toString(); -} - -function getBrowserEngineKind() { - var _a, _b; - // Based on research in October 2020. Tested to detect Chromium 42-86. - const w = window; - const n = navigator; - if (countTruthy([ - 'webkitPersistentStorage' in n, - 'webkitTemporaryStorage' in n, - n.vendor.indexOf('Google') === 0, - 'webkitResolveLocalFileSystemURL' in w, - 'BatteryManager' in w, - 'webkitMediaStream' in w, - 'webkitSpeechGrammar' in w, - ]) >= 5) { - return "chromium" /* BrowserEngineKind.Chromium */; - } - if (countTruthy([ - 'ApplePayError' in w, - 'CSSPrimitiveValue' in w, - 'Counter' in w, - n.vendor.indexOf('Apple') === 0, - 'getStorageUpdates' in n, - 'WebKitMediaKeys' in w, - ]) >= 4) { - return "webkit" /* BrowserEngineKind.Webkit */; - } - if (countTruthy([ - 'buildID' in navigator, - 'MozAppearance' in ((_b = (_a = document.documentElement) === null || _a === void 0 ? void 0 : _a.style) !== null && _b !== void 0 ? _b : {}), - 'onmozfullscreenchange' in w, - 'mozInnerScreenX' in w, - 'CSSMozDocumentRule' in w, - 'CanvasCaptureMediaStream' in w, - ]) >= 4) { - return "gecko" /* BrowserEngineKind.Gecko */; - } - return "unknown" /* BrowserEngineKind.Unknown */; -} -function getBrowserKind() { - var _a; - const userAgent = (_a = navigator.userAgent) === null || _a === void 0 ? void 0 : _a.toLowerCase(); - if (strIncludes(userAgent, 'edg/')) { - return "edge" /* BrowserKind.Edge */; - } - else if (strIncludes(userAgent, 'trident') || strIncludes(userAgent, 'msie')) { - return "internet_explorer" /* BrowserKind.IE */; - } - else if (strIncludes(userAgent, 'wechat')) { - return "wechat" /* BrowserKind.WeChat */; - } - else if (strIncludes(userAgent, 'firefox')) { - return "firefox" /* BrowserKind.Firefox */; - } - else if (strIncludes(userAgent, 'opera') || strIncludes(userAgent, 'opr')) { - return "opera" /* BrowserKind.Opera */; - } - else if (strIncludes(userAgent, 'chrome')) { - return "chrome" /* BrowserKind.Chrome */; - } - else if (strIncludes(userAgent, 'safari')) { - return "safari" /* BrowserKind.Safari */; - } - else { - return "unknown" /* BrowserKind.Unknown */; - } -} -// Source: https://github.com/fingerprintjs/fingerprintjs/blob/master/src/utils/browser.ts#L223 -function isAndroid() { - const browserEngineKind = getBrowserEngineKind(); - const isItChromium = browserEngineKind === "chromium" /* BrowserEngineKind.Chromium */; - const isItGecko = browserEngineKind === "gecko" /* BrowserEngineKind.Gecko */; - const w = window; - const n = navigator; - const c = 'connection'; - // Chrome removes all words "Android" from `navigator` when desktop version is requested - // Firefox keeps "Android" in `navigator.appVersion` when desktop version is requested - if (isItChromium) { - return (countTruthy([ - !('SharedWorker' in w), - // `typechange` is deprecated, but it's still present on Android (tested on Chrome Mobile 117) - // Removal proposal https://bugs.chromium.org/p/chromium/issues/detail?id=699892 - // Note: this expression returns true on ChromeOS, so additional detectors are required to avoid false-positives - n[c] && 'ontypechange' in n[c], - !('sinkId' in new Audio()), - ]) >= 2); - } - else if (isItGecko) { - return countTruthy(['onorientationchange' in w, 'orientation' in w, /android/i.test(n.appVersion)]) >= 2; - } - else { - // Only 2 browser engines are presented on Android. - // Actually, there is also Android 4.1 browser, but it's not worth detecting it at the moment. - return false; - } -} -function getDocumentFocus() { - if (document.hasFocus === undefined) { - return false; - } - return document.hasFocus(); -} -function isChromium86OrNewer() { - // Checked in Chrome 85 vs Chrome 86 both on desktop and Android. Checked in macOS Chrome 128, Android Chrome 127. - const w = window; - return (countTruthy([ - !('MediaSettingsRange' in w), - 'RTCEncodedAudioFrame' in w, - '' + w.Intl === '[object Intl]', - '' + w.Reflect === '[object Reflect]', - ]) >= 3); -} - -function getLanguages() { - const n = navigator; - const result = []; - const language = n.language || n.userLanguage || n.browserLanguage || n.systemLanguage; - if (language !== undefined) { - result.push([language]); - } - if (Array.isArray(n.languages)) { - const browserEngine = getBrowserEngineKind(); - // Starting from Chromium 86, there is only a single value in `navigator.language` in Incognito mode: - // the value of `navigator.language`. Therefore, the value is ignored in this browser. - if (!(browserEngine === "chromium" /* BrowserEngineKind.Chromium */ && isChromium86OrNewer())) { - result.push(n.languages); - } - } - else if (typeof n.languages === 'string') { - const languages = n.languages; - if (languages) { - result.push(languages.split(',')); - } - } - return result; -} - -function areMimeTypesConsistent() { - if (navigator.mimeTypes === undefined) { - throw new BotdError(-1 /* State.Undefined */, 'navigator.mimeTypes is undefined'); - } - const { mimeTypes } = navigator; - let isConsistent = Object.getPrototypeOf(mimeTypes) === MimeTypeArray.prototype; - for (let i = 0; i < mimeTypes.length; i++) { - isConsistent && (isConsistent = Object.getPrototypeOf(mimeTypes[i]) === MimeType.prototype); - } - return isConsistent; -} - -async function getNotificationPermissions() { - if (window.Notification === undefined) { - throw new BotdError(-1 /* State.Undefined */, 'window.Notification is undefined'); - } - if (navigator.permissions === undefined) { - throw new BotdError(-1 /* State.Undefined */, 'navigator.permissions is undefined'); - } - const { permissions } = navigator; - if (typeof permissions.query !== 'function') { - throw new BotdError(-2 /* State.NotFunction */, 'navigator.permissions.query is not a function'); - } - try { - const permissionStatus = await permissions.query({ name: 'notifications' }); - return window.Notification.permission === 'denied' && permissionStatus.state === 'prompt'; - } - catch (e) { - throw new BotdError(-3 /* State.UnexpectedBehaviour */, 'notificationPermissions signal unexpected behaviour'); - } -} - -function getPluginsArray() { - if (navigator.plugins === undefined) { - throw new BotdError(-1 /* State.Undefined */, 'navigator.plugins is undefined'); - } - if (window.PluginArray === undefined) { - throw new BotdError(-1 /* State.Undefined */, 'window.PluginArray is undefined'); - } - return navigator.plugins instanceof PluginArray; -} - -function getPluginsLength() { - if (navigator.plugins === undefined) { - throw new BotdError(-1 /* State.Undefined */, 'navigator.plugins is undefined'); - } - if (navigator.plugins.length === undefined) { - throw new BotdError(-3 /* State.UnexpectedBehaviour */, 'navigator.plugins.length is undefined'); - } - return navigator.plugins.length; -} - -function getProcess() { - const { process } = window; - const errorPrefix = 'window.process is'; - if (process === undefined) { - throw new BotdError(-1 /* State.Undefined */, `${errorPrefix} undefined`); - } - if (process && typeof process !== 'object') { - throw new BotdError(-3 /* State.UnexpectedBehaviour */, `${errorPrefix} not an object`); - } - return process; -} - -function getProductSub() { - const { productSub } = navigator; - if (productSub === undefined) { - throw new BotdError(-1 /* State.Undefined */, 'navigator.productSub is undefined'); - } - return productSub; -} - -function getRTT() { - if (navigator.connection === undefined) { - throw new BotdError(-1 /* State.Undefined */, 'navigator.connection is undefined'); - } - if (navigator.connection.rtt === undefined) { - throw new BotdError(-1 /* State.Undefined */, 'navigator.connection.rtt is undefined'); - } - return navigator.connection.rtt; -} - -function getUserAgent() { - return navigator.userAgent; -} - -function getWebDriver() { - if (navigator.webdriver == undefined) { - throw new BotdError(-1 /* State.Undefined */, 'navigator.webdriver is undefined'); - } - return navigator.webdriver; -} - -function getWebGL() { - const canvasElement = document.createElement('canvas'); - if (typeof canvasElement.getContext !== 'function') { - throw new BotdError(-2 /* State.NotFunction */, 'HTMLCanvasElement.getContext is not a function'); - } - const webGLContext = canvasElement.getContext('webgl'); - if (webGLContext === null) { - throw new BotdError(-4 /* State.Null */, 'WebGLRenderingContext is null'); - } - if (typeof webGLContext.getParameter !== 'function') { - throw new BotdError(-2 /* State.NotFunction */, 'WebGLRenderingContext.getParameter is not a function'); - } - const vendor = webGLContext.getParameter(webGLContext.VENDOR); - const renderer = webGLContext.getParameter(webGLContext.RENDERER); - return { vendor: vendor, renderer: renderer }; -} - -function getWindowExternal() { - if (window.external === undefined) { - throw new BotdError(-1 /* State.Undefined */, 'window.external is undefined'); - } - const { external } = window; - if (typeof external.toString !== 'function') { - throw new BotdError(-2 /* State.NotFunction */, 'window.external.toString is not a function'); - } - return external.toString(); -} - -function getWindowSize() { - return { - outerWidth: window.outerWidth, - outerHeight: window.outerHeight, - innerWidth: window.innerWidth, - innerHeight: window.innerHeight, - }; -} - -function checkDistinctiveProperties() { - // The order in the following list matters, because specific types of bots come first, followed by automation technologies. - const distinctivePropsList = { - [BotKind.Awesomium]: { - window: ['awesomium'], - }, - [BotKind.Cef]: { - window: ['RunPerfTest'], - }, - [BotKind.CefSharp]: { - window: ['CefSharp'], - }, - [BotKind.CoachJS]: { - window: ['emit'], - }, - [BotKind.FMiner]: { - window: ['fmget_targets'], - }, - [BotKind.Geb]: { - window: ['geb'], - }, - [BotKind.NightmareJS]: { - window: ['__nightmare', 'nightmare'], - }, - [BotKind.Phantomas]: { - window: ['__phantomas'], - }, - [BotKind.PhantomJS]: { - window: ['callPhantom', '_phantom'], - }, - [BotKind.Rhino]: { - window: ['spawn'], - }, - [BotKind.Selenium]: { - window: ['_Selenium_IDE_Recorder', '_selenium', 'calledSelenium', /^([a-z]){3}_.*_(Array|Promise|Symbol)$/], - document: ['__selenium_evaluate', 'selenium-evaluate', '__selenium_unwrapped'], - }, - [BotKind.WebDriverIO]: { - window: ['wdioElectron'], - }, - [BotKind.WebDriver]: { - window: [ - 'webdriver', - '__webdriverFunc', - '__lastWatirAlert', - '__lastWatirConfirm', - '__lastWatirPrompt', - '_WEBDRIVER_ELEM_CACHE', - 'ChromeDriverw', - ], - document: [ - '__webdriver_script_fn', - '__driver_evaluate', - '__webdriver_evaluate', - '__fxdriver_evaluate', - '__driver_unwrapped', - '__webdriver_unwrapped', - '__fxdriver_unwrapped', - '__webdriver_script_fn', - '__webdriver_script_func', - '__webdriver_script_function', - '$cdc_asdjflasutopfhvcZLmcf', - '$cdc_asdjflasutopfhvcZLmcfl_', - '$chrome_asyncScriptInfo', - '__$webdriverAsyncExecutor', - ], - }, - [BotKind.HeadlessChrome]: { - window: ['domAutomation', 'domAutomationController'], - }, - }; - let botName; - const result = {}; - const windowProps = getObjectProps(window); - let documentProps = []; - if (window.document !== undefined) - documentProps = getObjectProps(window.document); - for (botName in distinctivePropsList) { - const props = distinctivePropsList[botName]; - if (props !== undefined) { - const windowContains = props.window === undefined ? false : includes(windowProps, ...props.window); - const documentContains = props.document === undefined || !documentProps.length ? false : includes(documentProps, ...props.document); - result[botName] = windowContains || documentContains; - } - } - return result; -} - -const sources = { - android: isAndroid, - browserKind: getBrowserKind, - browserEngineKind: getBrowserEngineKind, - documentFocus: getDocumentFocus, - userAgent: getUserAgent, - appVersion: getAppVersion, - rtt: getRTT, - windowSize: getWindowSize, - pluginsLength: getPluginsLength, - pluginsArray: getPluginsArray, - errorTrace: getErrorTrace, - productSub: getProductSub, - windowExternal: getWindowExternal, - mimeTypesConsistent: areMimeTypesConsistent, - evalLength: getEvalLength, - webGL: getWebGL, - webDriver: getWebDriver, - languages: getLanguages, - notificationPermissions: getNotificationPermissions, - documentElementKeys: getDocumentElementKeys, - functionBind: getFunctionBind, - process: getProcess, - distinctiveProps: checkDistinctiveProperties, -}; - -/** - * Class representing a bot detector. - * - * @class - * @implements {BotDetectorInterface} - */ -class BotDetector { - constructor() { - this.components = undefined; - this.detections = undefined; - } - getComponents() { - return this.components; - } - getDetections() { - return this.detections; - } - /** - * @inheritdoc - */ - detect() { - if (this.components === undefined) { - throw new Error("BotDetector.detect can't be called before BotDetector.collect"); - } - const [detections, finalDetection] = detect(this.components, detectors); - this.detections = detections; - return finalDetection; - } - /** - * @inheritdoc - */ - async collect() { - this.components = await collect(sources); - return this.components; - } -} - -/** - * Sends an unpersonalized AJAX request to collect installation statistics - */ -function monitor() { - // The FingerprintJS CDN (https://github.com/fingerprintjs/cdn) replaces `window.__fpjs_d_m` with `true` - if (window.__fpjs_d_m || Math.random() >= 0.001) { - return; - } - try { - const request = new XMLHttpRequest(); - request.open('get', `https://m1.openfpcdn.io/botd/v${version}/npm-monitoring`, true); - request.send(); - } - catch (error) { - // console.error is ok here because it's an unexpected error handler - // eslint-disable-next-line no-console - console.error(error); - } -} -async function load({ monitoring = true } = {}) { - if (monitoring) { - monitor(); - } - const detector = new BotDetector(); - await detector.collect(); - return detector; -} -var index = { load }; - -export { BotKind, BotdError, collect, index as default, detect, detectors, load, sources }; diff --git a/tests/vendor/creepjs-10aa672.js b/tests/vendor/creepjs-10aa672.js deleted file mode 100644 index 1e920f1..0000000 --- a/tests/vendor/creepjs-10aa672.js +++ /dev/null @@ -1,9710 +0,0 @@ -(function () { - 'use strict'; - - var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null; - // @ts-expect-error - const IS_WORKER_SCOPE = !self.document && self.WorkerGlobalScope; - // Detect Browser - function getEngine() { - const x = [].constructor; - try { - (-1).toFixed(-1); - } - catch (err) { - return err.message.length + (x + '').split(x.name).join('').length; - } - } - const ENGINE_IDENTIFIER = getEngine(); - const IS_BLINK = ENGINE_IDENTIFIER == 80; - const IS_GECKO = ENGINE_IDENTIFIER == 58; - const IS_WEBKIT = ENGINE_IDENTIFIER == 77; - const JS_ENGINE = ({ - 80: 'V8', - 58: 'SpiderMonkey', - 77: 'JavaScriptCore', - })[ENGINE_IDENTIFIER] || null; - const LIKE_BRAVE = IS_BLINK && 'flat' in Array.prototype /* Chrome 69 */ && !('ReportingObserver' in self /* Brave */); - function braveBrowser() { - const brave = ('brave' in navigator && - // @ts-ignore - Object.getPrototypeOf(navigator.brave).constructor.name == 'Brave' && - // @ts-ignore - navigator.brave.isBrave.toString() == 'function isBrave() { [native code] }'); - return brave; - } - function getBraveMode() { - const mode = { - unknown: false, - allow: false, - standard: false, - strict: false, - }; - try { - // strict mode adds float frequency data AnalyserNode - const strictMode = () => { - try { - window.OfflineAudioContext = ( - // @ts-ignore - OfflineAudioContext || webkitOfflineAudioContext); - } - catch (err) { } - if (!window.OfflineAudioContext) { - return false; - } - const context = new OfflineAudioContext(1, 1, 44100); - const analyser = context.createAnalyser(); - const data = new Float32Array(analyser.frequencyBinCount); - analyser.getFloatFrequencyData(data); - const strict = new Set(data).size > 1; // native only has -Infinity - return strict; - }; - if (strictMode()) { - mode.strict = true; - return mode; - } - // standard and strict mode do not have chrome plugins - const chromePlugins = /(Chrom(e|ium)|Microsoft Edge) PDF (Plugin|Viewer)/; - const pluginsList = [...navigator.plugins]; - const hasChromePlugins = pluginsList - .filter((plugin) => chromePlugins.test(plugin.name)).length == 2; - if (pluginsList.length && !hasChromePlugins) { - mode.standard = true; - return mode; - } - mode.allow = true; - return mode; - } - catch (e) { - mode.unknown = true; - return mode; - } - } - const getBraveUnprotectedParameters = (parameters) => { - const blocked = new Set([ - 'FRAGMENT_SHADER.HIGH_FLOAT.precision', - 'FRAGMENT_SHADER.HIGH_FLOAT.rangeMax', - 'FRAGMENT_SHADER.HIGH_FLOAT.rangeMin', - 'FRAGMENT_SHADER.HIGH_INT.precision', - 'FRAGMENT_SHADER.HIGH_INT.rangeMax', - 'FRAGMENT_SHADER.HIGH_INT.rangeMin', - 'FRAGMENT_SHADER.LOW_FLOAT.precision', - 'FRAGMENT_SHADER.LOW_FLOAT.rangeMax', - 'FRAGMENT_SHADER.LOW_FLOAT.rangeMin', - 'FRAGMENT_SHADER.MEDIUM_FLOAT.precision', - 'FRAGMENT_SHADER.MEDIUM_FLOAT.rangeMax', - 'FRAGMENT_SHADER.MEDIUM_FLOAT.rangeMin', - 'MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS', - 'MAX_COMBINED_UNIFORM_BLOCKS', - 'MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS', - 'MAX_DRAW_BUFFERS_WEBGL', - 'MAX_FRAGMENT_INPUT_COMPONENTS', - 'MAX_FRAGMENT_UNIFORM_BLOCKS', - 'MAX_FRAGMENT_UNIFORM_COMPONENTS', - 'MAX_TEXTURE_MAX_ANISOTROPY_EXT', - 'MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS', - 'MAX_UNIFORM_BUFFER_BINDINGS', - 'MAX_VARYING_COMPONENTS', - 'MAX_VERTEX_OUTPUT_COMPONENTS', - 'MAX_VERTEX_UNIFORM_BLOCKS', - 'MAX_VERTEX_UNIFORM_COMPONENTS', - 'SHADING_LANGUAGE_VERSION', - 'UNMASKED_RENDERER_WEBGL', - 'UNMASKED_VENDOR_WEBGL', - 'VERSION', - 'VERTEX_SHADER.HIGH_FLOAT.precision', - 'VERTEX_SHADER.HIGH_FLOAT.rangeMax', - 'VERTEX_SHADER.HIGH_FLOAT.rangeMin', - 'VERTEX_SHADER.HIGH_INT.precision', - 'VERTEX_SHADER.HIGH_INT.rangeMax', - 'VERTEX_SHADER.HIGH_INT.rangeMin', - 'VERTEX_SHADER.LOW_FLOAT.precision', - 'VERTEX_SHADER.LOW_FLOAT.rangeMax', - 'VERTEX_SHADER.LOW_FLOAT.rangeMin', - 'VERTEX_SHADER.MEDIUM_FLOAT.precision', - 'VERTEX_SHADER.MEDIUM_FLOAT.rangeMax', - 'VERTEX_SHADER.MEDIUM_FLOAT.rangeMin', - ]); - const safeParameters = Object.keys(parameters).reduce((acc, curr) => { - if (blocked.has(curr)) { - return acc; - } - acc[curr] = parameters[curr]; - return acc; - }, {}); - return safeParameters; - }; - // system - const getOS = (userAgent) => { - const os = ( - // order is important - /windows phone/ig.test(userAgent) ? 'Windows Phone' : - /win(dows|16|32|64|95|98|nt)|wow64/ig.test(userAgent) ? 'Windows' : - /android/ig.test(userAgent) ? 'Android' : - /cros/ig.test(userAgent) ? 'Chrome OS' : - /linux/ig.test(userAgent) ? 'Linux' : - /ipad/ig.test(userAgent) ? 'iPad' : - /iphone/ig.test(userAgent) ? 'iPhone' : - /ipod/ig.test(userAgent) ? 'iPod' : - /ios/ig.test(userAgent) ? 'iOS' : - /mac/ig.test(userAgent) ? 'Mac' : - 'Other'); - return os; - }; - function getReportedPlatform(userAgent, platform) { - // user agent os lie - const userAgentOS = ( - // order is important - /win(dows|16|32|64|95|98|nt)|wow64/ig.test(userAgent) ? "Windows" /* PlatformClassifier.WINDOWS */ : - /android|linux|cros/ig.test(userAgent) ? "Linux" /* PlatformClassifier.LINUX */ : - /(i(os|p(ad|hone|od)))|mac/ig.test(userAgent) ? "Apple" /* PlatformClassifier.APPLE */ : - "Other" /* PlatformClassifier.OTHER */); - if (!platform) - return [userAgentOS]; - const platformOS = ( - // order is important - /win/ig.test(platform) ? "Windows" /* PlatformClassifier.WINDOWS */ : - /android|arm|linux/ig.test(platform) ? "Linux" /* PlatformClassifier.LINUX */ : - /(i(os|p(ad|hone|od)))|mac/ig.test(platform) ? "Apple" /* PlatformClassifier.APPLE */ : - "Other" /* PlatformClassifier.OTHER */); - return [userAgentOS, platformOS]; - } - const { userAgent: navUserAgent, platform: navPlatform } = self.navigator || {}; - const [USER_AGENT_OS, PLATFORM_OS] = getReportedPlatform(navUserAgent, navPlatform); - const decryptUserAgent = ({ ua, os, isBrave }) => { - const apple = /ipad|iphone|ipod|ios|mac/ig.test(os); - const isOpera = /OPR\//g.test(ua); - const isVivaldi = /Vivaldi/g.test(ua); - const isDuckDuckGo = /DuckDuckGo/g.test(ua); - const isYandex = /YaBrowser/g.test(ua); - const paleMoon = ua.match(/(palemoon)\/(\d+)./i); - const edge = ua.match(/(edgios|edg|edge|edga)\/(\d+)./i); - const edgios = edge && /edgios/i.test(edge[1]); - const chromium = ua.match(/(crios|chrome)\/(\d+)./i); - const firefox = ua.match(/(fxios|firefox)\/(\d+)./i); - const likeSafari = (/AppleWebKit/g.test(ua) && - /Safari/g.test(ua)); - const safari = (likeSafari && - !firefox && - !chromium && - !edge && - ua.match(/(version)\/(\d+)\.(\d|\.)+\s(mobile|safari)/i)); - if (chromium) { - const browser = chromium[1]; - const version = chromium[2]; - const like = (isOpera ? ' Opera' : - isVivaldi ? ' Vivaldi' : - isDuckDuckGo ? ' DuckDuckGo' : - isYandex ? ' Yandex' : - edge ? ' Edge' : - isBrave ? ' Brave' : ''); - return `${browser} ${version}${like}`; - } - else if (edgios) { - const browser = edge[1]; - const version = edge[2]; - return `${browser} ${version}`; - } - else if (firefox) { - const browser = paleMoon ? paleMoon[1] : firefox[1]; - const version = paleMoon ? paleMoon[2] : firefox[2]; - return `${browser} ${version}`; - } - else if (apple && safari) { - const browser = 'Safari'; - const version = safari[2]; - return `${browser} ${version}`; - } - return 'unknown'; - }; - const getUserAgentPlatform = ({ userAgent, excludeBuild = true }) => { - if (!userAgent) { - return 'unknown'; - } - // patterns - const nonPlatformParenthesis = /\((khtml|unlike|vizio|like gec|internal dummy|org\.eclipse|openssl|ipv6|via translate|safari|cardamon).+|xt\d+\)/ig; - const parenthesis = /\((.+)\)/; - const android = /((android).+)/i; - const androidNoise = /^(linux|[a-z]|wv|mobile|[a-z]{2}(-|_)[a-z]{2}|[a-z]{2})$|windows|(rv:|trident|webview|iemobile).+/i; - const androidBuild = /build\/.+\s|\sbuild\/.+/i; - const androidRelease = /android( |-)\d+/i; - const windows = /((windows).+)/i; - const windowsNoise = /^(windows|ms(-|)office|microsoft|compatible|[a-z]|x64|[a-z]{2}(-|_)[a-z]{2}|[a-z]{2})$|(rv:|outlook|ms(-|)office|microsoft|trident|\.net|msie|httrack|media center|infopath|aol|opera|iemobile|webbrowser).+/i; - const windows64bitCPU = /w(ow|in)64/i; - const cros = /cros/i; - const crosNoise = /^([a-z]|x11|[a-z]{2}(-|_)[a-z]{2}|[a-z]{2})$|(rv:|trident).+/i; - const crosBuild = /\d+\.\d+\.\d+/i; - const linux = /linux|x11|ubuntu|debian/i; - const linuxNoise = /^([a-z]|x11|unknown|compatible|[a-z]{2}(-|_)[a-z]{2}|[a-z]{2})$|(rv:|java|oracle|\+http|http|unknown|mozilla|konqueror|valve).+/i; - const apple = /(cpu iphone|cpu os|iphone os|mac os|macos|intel os|ppc mac).+/i; - const appleNoise = /^([a-z]|macintosh|compatible|mimic|[a-z]{2}(-|_)[a-z]{2}|[a-z]{2}|rv|\d+\.\d+)$|(rv:|silk|valve).+/i; - const appleRelease = /(ppc |intel |)(mac|mac |)os (x |x|)(\d{2}(_|\.)\d{1,2}|\d{2,})/i; - const otherOS = /((symbianos|nokia|blackberry|morphos|mac).+)|\/linux|freebsd|symbos|series \d+|win\d+|unix|hp-ux|bsdi|bsd|x86_64/i; - const isDevice = (list, device) => list.filter((x) => device.test(x)).length; - userAgent = userAgent.trim().replace(/\s{2,}/, ' ').replace(nonPlatformParenthesis, ''); - if (parenthesis.test(userAgent)) { - const platformSection = userAgent.match(parenthesis)[0]; - const identifiers = platformSection.slice(1, -1).replace(/,/g, ';').split(';').map((x) => x.trim()); - if (isDevice(identifiers, android)) { - return identifiers - // @ts-ignore - .map((x) => androidRelease.test(x) ? androidRelease.exec(x)[0].replace('-', ' ') : x) - .filter((x) => !(androidNoise.test(x))) - .join(' ') - .replace((excludeBuild ? androidBuild : ''), '') - .trim().replace(/\s{2,}/, ' '); - } - else if (isDevice(identifiers, windows)) { - return identifiers - .filter((x) => !(windowsNoise.test(x))) - .join(' ') - .replace(/\sNT (\d+\.\d+)/, (match, version) => { - return (version == '10.0' ? ' 10' : - version == '6.3' ? ' 8.1' : - version == '6.2' ? ' 8' : - version == '6.1' ? ' 7' : - version == '6.0' ? ' Vista' : - version == '5.2' ? ' XP Pro' : - version == '5.1' ? ' XP' : - version == '5.0' ? ' 2000' : - version == '4.0' ? match : - ' ' + version); - }) - .replace(windows64bitCPU, '(64-bit)') - .trim().replace(/\s{2,}/, ' '); - } - else if (isDevice(identifiers, cros)) { - return identifiers - .filter((x) => !(crosNoise.test(x))) - .join(' ') - .replace((excludeBuild ? crosBuild : ''), '') - .trim().replace(/\s{2,}/, ' '); - } - else if (isDevice(identifiers, linux)) { - return identifiers - .filter((x) => !(linuxNoise.test(x))) - .join(' ') - .trim().replace(/\s{2,}/, ' '); - } - else if (isDevice(identifiers, apple)) { - return identifiers - .map((x) => { - if (appleRelease.test(x)) { - // @ts-ignore - const release = appleRelease.exec(x)[0]; - const versionMap = { - '10_7': 'Lion', - '10_8': 'Mountain Lion', - '10_9': 'Mavericks', - '10_10': 'Yosemite', - '10_11': 'El Capitan', - '10_12': 'Sierra', - '10_13': 'High Sierra', - '10_14': 'Mojave', - '10_15': 'Catalina', - '11': 'Big Sur', - '12': 'Monterey', - '13': 'Ventura', - }; - const version = ((/(\d{2}(_|\.)\d{1,2}|\d{2,})/.exec(release) || [])[0] || - '').replace(/\./g, '_'); - const isOSX = /^10/.test(version); - const id = isOSX ? version : (/^\d{2,}/.exec(version) || [])[0]; - const codeName = versionMap[id]; - return codeName ? `macOS ${codeName}` : release; - } - return x; - }) - .filter((x) => !(appleNoise.test(x))) - .join(' ') - .replace(/\slike mac.+/ig, '') - .trim().replace(/\s{2,}/, ' '); - } - else { - const other = identifiers.filter((x) => otherOS.test(x)); - if (other.length) { - return other.join(' ').trim().replace(/\s{2,}/, ' '); - } - return identifiers.join(' '); - } - } - else { - return 'unknown'; - } - }; - const computeWindowsRelease = ({ platform, platformVersion, fontPlatformVersion }) => { - if ((platform != 'Windows') || !(IS_BLINK && CSS.supports('accent-color', 'initial'))) { - return; - } - const platformVersionNumber = +(/(\d+)\./.exec(platformVersion) || [])[1]; - // https://github.com/WICG/ua-client-hints/issues/220#issuecomment-870858413 - // https://docs.microsoft.com/en-us/microsoft-edge/web-platform/how-to-detect-win11 - // https://docs.microsoft.com/en-us/microsoft-edge/web-platform/user-agent-guidance - const release = { - '0.1.0': '7', - '0.2.0': '8', - '0.3.0': '8.1', - '1.0.0': '10 (1507)', - '2.0.0': '10 (1511)', - '3.0.0': '10 (1607)', - '4.0.0': '10 (1703)', - '5.0.0': '10 (1709)', - '6.0.0': '10 (1803)', - '7.0.0': '10 (1809)', - '8.0.0': '10 (1903|1909)', - '10.0.0': '10 (2004|20H2|21H1)', - '11.0.0': '10', - '12.0.0': '10', - }; - const oldFontPlatformVersionNumber = (/7|8\.1|8/.exec(fontPlatformVersion) || [])[0]; - const version = (platformVersionNumber >= 13 ? '11' : - platformVersionNumber == 0 && oldFontPlatformVersionNumber ? oldFontPlatformVersionNumber : - (release[platformVersion] || 'Unknown')); - return (`Windows ${version} [${platformVersion}]`); - }; - // attempt restore from User-Agent Reduction - const isUAPostReduction = (userAgent) => { - const matcher = /Mozilla\/5\.0 \((Macintosh; Intel Mac OS X 10_15_7|Windows NT 10\.0; Win64; x64|(X11; (CrOS|Linux) x86_64)|(Linux; Android 10(; K|)))\) AppleWebKit\/537\.36 \(KHTML, like Gecko\) Chrome\/\d+\.0\.0\.0( Mobile|) Safari\/537\.36/; - const unifiedPlatform = (matcher.exec(userAgent) || [])[1]; - return IS_BLINK && !!unifiedPlatform; - }; - const createPerformanceLogger = () => { - const log = {}; - let total = 0; - return { - logTestResult: ({ test, passed, time = 0 }) => { - total += time; - const timeString = `${time.toFixed(2)}ms`; - log[test] = timeString; - const color = passed ? '#4cca9f' : 'lightcoral'; - const result = passed ? 'passed' : 'failed'; - const symbol = passed ? '✔' : '-'; - return console.log(`%c${symbol}${time ? ` (${timeString})` : ''} ${test} ${result}`, `color:${color}`); - }, - getLog: () => log, - getTotal: () => total, - }; - }; - const performanceLogger = createPerformanceLogger(); - const { logTestResult } = performanceLogger; - const createTimer = () => { - let start = 0; - const log = []; - return { - stop: () => { - if (start) { - log.push(performance.now() - start); - return log.reduce((acc, n) => acc += n, 0); - } - return start; - }, - start: () => { - start = performance.now(); - return start; - }, - }; - }; - const queueEvent = (timer, delay = 0) => { - timer.stop(); - return new Promise((resolve) => setTimeout(() => resolve(timer.start()), delay)) - .catch((e) => { }); - }; - const formatEmojiSet = (emojiSet, limit = 3) => { - const maxLen = (limit * 2) + 3; - const list = (emojiSet || []); - return list.length > maxLen ? `${emojiSet.slice(0, limit).join('')}...${emojiSet.slice(-limit).join('')}` : - list.join(''); - }; - const EMOJIS = [ - [128512], [9786], [129333, 8205, 9794, 65039], [9832], [9784], [9895], [8265], [8505], [127987, 65039, 8205, 9895, 65039], [129394], [9785], [9760], [129489, 8205, 129456], [129487, 8205, 9794, 65039], [9975], [129489, 8205, 129309, 8205, 129489], [9752], [9968], [9961], [9972], [9992], [9201], [9928], [9730], [9969], [9731], [9732], [9976], [9823], [9937], [9000], [9993], [9999], - [128105, 8205, 10084, 65039, 8205, 128139, 8205, 128104], - [128104, 8205, 128105, 8205, 128103, 8205, 128102], - [128104, 8205, 128105, 8205, 128102], - // android 11 - [128512], - [169], [174], [8482], - [128065, 65039, 8205, 128488, 65039], - // other - [10002], [9986], [9935], [9874], [9876], [9881], [9939], [9879], [9904], [9905], [9888], [9762], [9763], [11014], [8599], [10145], [11013], [9883], [10017], [10013], [9766], [9654], [9197], [9199], [9167], [9792], [9794], [10006], [12336], [9877], [9884], [10004], [10035], [10055], [9724], [9642], [10083], [10084], [9996], [9757], [9997], [10052], [9878], [8618], [9775], [9770], [9774], [9745], [10036], [127344], [127359], - ].map((emojiCode) => String.fromCodePoint(...emojiCode)); - const CSS_FONT_FAMILY = ` - 'Segoe Fluent Icons', - 'Ink Free', - 'Bahnschrift', - 'Segoe MDL2 Assets', - 'HoloLens MDL2 Assets', - 'Leelawadee UI', - 'Javanese Text', - 'Segoe UI Emoji', - 'Aldhabi', - 'Gadugi', - 'Myanmar Text', - 'Nirmala UI', - 'Lucida Console', - 'Cambria Math', - 'Bai Jamjuree', - 'Chakra Petch', - 'Charmonman', - 'Fahkwang', - 'K2D', - 'Kodchasan', - 'KoHo', - 'Sarabun', - 'Srisakdi', - 'Galvji', - 'MuktaMahee Regular', - 'InaiMathi Bold', - 'American Typewriter Semibold', - 'Futura Bold', - 'SignPainter-HouseScript Semibold', - 'PingFang HK Light', - 'Kohinoor Devanagari Medium', - 'Luminari', - 'Geneva', - 'Helvetica Neue', - 'Droid Sans Mono', - 'Dancing Script', - 'Roboto', - 'Ubuntu', - 'Liberation Mono', - 'Source Code Pro', - 'DejaVu Sans', - 'OpenSymbol', - 'Chilanka', - 'Cousine', - 'Arimo', - 'Jomolhari', - 'MONO', - 'Noto Color Emoji', - sans-serif !important -`; - const hashSlice = (x) => !x ? x : x.slice(0, 8); - function getGpuBrand(gpu) { - if (!gpu) - return null; - const gpuBrandMatcher = /(adreno|amd|apple|intel|llvm|mali|microsoft|nvidia|parallels|powervr|samsung|swiftshader|virtualbox|vmware)/i; - const brand = (/radeon/i.test(gpu) ? 'AMD' : - /geforce/i.test(gpu) ? 'NVIDIA' : - (gpuBrandMatcher.exec(gpu)?.[0] || 'other').toLocaleUpperCase()); - return brand; - } - // collect fingerprints for analysis - const Analysis = {}; - // use if needed to stable fingerprint - const LowerEntropy = { - AUDIO: false, - CANVAS: false, - FONTS: false, - SCREEN: false, - TIME_ZONE: false, - WEBGL: false, - }; - - // template views - function patch(oldEl, newEl, fn) { - if (!oldEl) - return null; - oldEl.parentNode?.replaceChild(newEl, oldEl); - return typeof fn === 'function' ? fn() : true; - } - function html(templateStr, ...expressionSet) { - const template = document.createElement('template'); - template.innerHTML = templateStr.map((s, i) => `${s}${expressionSet[i] || ''}`).join(''); - return document.importNode(template.content, true); - } - // template helpers - const HTMLNote = { - UNKNOWN: 'unknown', - UNSUPPORTED: 'unsupported', - BLOCKED: 'blocked', - LIED: 'lied', - SECRET: 'secret', - }; - const count = (arr) => arr && arr.constructor.name === 'Array' ? '' + (arr.length) : '0'; - const getDiffs = ({ stringA, stringB, charDiff = false, decorate = (diff) => `[${diff}]` }) => { - if (!stringA || !stringB) - return; - const splitter = charDiff ? '' : ' '; - const listA = ('' + stringA).split(splitter); - const listB = ('' + stringB).split(splitter); - const listBWithDiffs = listB.map((x, i) => { - const matcher = listA[i]; - const match = x == matcher; - return !match ? decorate(x) : x; - }); - return listBWithDiffs.join(splitter); - }; - // modal component - const modal = (name, result, linkname = 'details') => { - if (!result.length) { - return ''; - } - return ` - - - - - `; - }; - - const createErrorsCaptured = () => { - const errors = []; - return { - getErrors: () => errors, - captureError: (error, customMessage = '') => { - const type = { - Error: true, - EvalError: true, - InternalError: true, - RangeError: true, - ReferenceError: true, - SyntaxError: true, - TypeError: true, - URIError: true, - InvalidStateError: true, - SecurityError: true, - }; - const hasInnerSpace = (s) => /.+(\s).+/g.test(s); // ignore AOPR noise - console.error(error); // log error to educate - const { name, message } = error; - const trustedMessage = (!hasInnerSpace(message) ? undefined : - !customMessage ? message : - `${message} [${customMessage}]`); - const trustedName = type[name] ? name : undefined; - errors.push({ trustedName, trustedMessage }); - return undefined; - }, - }; - }; - const errorsCaptured = createErrorsCaptured(); - const { captureError } = errorsCaptured; - const attempt = (fn, customMessage = '') => { - try { - return fn(); - } - catch (error) { - if (customMessage) { - return captureError(error, customMessage); - } - return captureError(error); - } - }; - const caniuse = (fn, objChainList = [], args = [], method = false) => { - let api; - try { - api = fn(); - } - catch (error) { - return undefined; - } - let i; - const len = objChainList.length; - let chain = api; - try { - for (i = 0; i < len; i++) { - const obj = objChainList[i]; - chain = chain[obj]; - } - } - catch (error) { - return undefined; - } - return (method && args.length ? chain.apply(api, args) : - method && !args.length ? chain.apply(api) : - chain); - }; - // Log performance time - const timer = (logStart) => { - let start = 0; - try { - start = performance.now(); - } - catch (error) { - captureError(error); - } - return (logEnd) => { - let end = 0; - try { - end = performance.now() - start; - logEnd && console.log(`${logEnd}: ${end / 1000} seconds`); - return end; - } - catch (error) { - captureError(error); - return 0; - } - }; - }; - const getCapturedErrors = () => ({ data: errorsCaptured.getErrors() }); - - /* eslint-disable new-cap */ - /* eslint-disable no-unused-vars */ - // warm up while we detect lies - try { - speechSynthesis.getVoices(); - } - catch (err) { } - // Collect lies detected - function createLieRecords() { - const records = {}; - return { - getRecords: () => records, - documentLie: (name, lie) => { - const isArray = lie instanceof Array; - if (records[name]) { - if (isArray) { - return (records[name] = [...records[name], ...lie]); - } - return records[name].push(lie); - } - return isArray ? (records[name] = lie) : (records[name] = [lie]); - }, - }; - } - const lieRecords = createLieRecords(); - const { documentLie } = lieRecords; - const GHOST = ` - height: 100vh; - width: 100vw; - position: absolute; - left:-10000px; - visibility: hidden; -`; - function getRandomValues() { - return (String.fromCharCode(Math.random() * 26 + 97) + - Math.random().toString(36).slice(-7)); - } - function getBehemothIframe(win) { - try { - if (!IS_BLINK) - return win; - const div = win.document.createElement('div'); - div.setAttribute('id', getRandomValues()); - div.setAttribute('style', GHOST); - div.innerHTML = `
`; - win.document.body.appendChild(div); - const iframe = [...[...div.childNodes][0].childNodes][0]; - if (!iframe) - return null; - const { contentWindow } = iframe || {}; - if (!contentWindow) - return null; - const div2 = contentWindow.document.createElement('div'); - div2.innerHTML = `
`; - contentWindow.document.body.appendChild(div2); - const iframe2 = [...[...div2.childNodes][0].childNodes][0]; - return iframe2.contentWindow; - } - catch (error) { - captureError(error, 'client blocked behemoth iframe'); - return win; - } - } - const RAND = getRandomValues(); - const HAS_REFLECT = 'Reflect' in self; - function isTypeError(err) { - return err.constructor.name == 'TypeError'; - } - function failsTypeError({ spawnErr, withStack, final }) { - try { - spawnErr(); - throw Error(); - } - catch (err) { - if (!isTypeError(err)) - return true; - return withStack ? withStack(err) : false; - } - finally { - final && final(); - } - } - function failsWithError(fn) { - try { - fn(); - return false; - } - catch (err) { - return true; - } - } - function hasKnownToString(name) { - return { - [`function ${name}() { [native code] }`]: true, - [`function get ${name}() { [native code] }`]: true, - [`function () { [native code] }`]: true, - [`function ${name}() {${'\n'} [native code]${'\n'}}`]: true, - [`function get ${name}() {${'\n'} [native code]${'\n'}}`]: true, - [`function () {${'\n'} [native code]${'\n'}}`]: true, - }; - } - function hasValidStack(err, reg, i = 1) { - if (i === 0) - return reg.test(err.message); - return reg.test(err.stack.split('\n')[i]); - } - const AT_FUNCTION = /at Function\.toString /; - const AT_OBJECT = /at Object\.toString/; - const FUNCTION_INSTANCE = /at (Function\.)?\[Symbol.hasInstance\]/; // useful if < Chrome 102 - const PROXY_INSTANCE = /at (Proxy\.)?\[Symbol.hasInstance\]/; // useful if < Chrome 102 - const STRICT_MODE = /strict mode/; - function queryLies({ scope, apiFunction, proto, obj, lieProps, }) { - if (typeof apiFunction != 'function') { - return { - lied: 0, - lieTypes: [], - }; - } - const name = apiFunction.name.replace(/get\s/, ''); - const objName = obj?.name; - const nativeProto = Object.getPrototypeOf(apiFunction); - let lies = { - // custom lie string names - ['failed illegal error']: !!obj && failsTypeError({ - spawnErr: () => obj.prototype[name], - }), - ['failed undefined properties']: (!!obj && /^(screen|navigator)$/i.test(objName) && !!(Object.getOwnPropertyDescriptor(self[objName.toLowerCase()], name) || (HAS_REFLECT && - Reflect.getOwnPropertyDescriptor(self[objName.toLowerCase()], name)))), - ['failed call interface error']: failsTypeError({ - spawnErr: () => { - // @ts-expect-error - new apiFunction(); - apiFunction.call(proto); - }, - }), - ['failed apply interface error']: failsTypeError({ - spawnErr: () => { - // @ts-expect-error - new apiFunction(); - apiFunction.apply(proto); - }, - }), - ['failed new instance error']: failsTypeError({ - // @ts-expect-error - spawnErr: () => new apiFunction(), - }), - ['failed class extends error']: !IS_WEBKIT && failsTypeError({ - spawnErr: () => { - // @ts-expect-error - class Fake extends apiFunction { - } - }, - }), - ['failed null conversion error']: failsTypeError({ - spawnErr: () => Object.setPrototypeOf(apiFunction, null).toString(), - final: () => Object.setPrototypeOf(apiFunction, nativeProto), - }), - ['failed toString']: (!hasKnownToString(name)[scope.Function.prototype.toString.call(apiFunction)] || - !hasKnownToString('toString')[scope.Function.prototype.toString.call(apiFunction.toString)]), - ['failed "prototype" in function']: 'prototype' in apiFunction, - ['failed descriptor']: !!(Object.getOwnPropertyDescriptor(apiFunction, 'arguments') || - Reflect.getOwnPropertyDescriptor(apiFunction, 'arguments') || - Object.getOwnPropertyDescriptor(apiFunction, 'caller') || - Reflect.getOwnPropertyDescriptor(apiFunction, 'caller') || - Object.getOwnPropertyDescriptor(apiFunction, 'prototype') || - Reflect.getOwnPropertyDescriptor(apiFunction, 'prototype') || - Object.getOwnPropertyDescriptor(apiFunction, 'toString') || - Reflect.getOwnPropertyDescriptor(apiFunction, 'toString')), - ['failed own property']: !!(apiFunction.hasOwnProperty('arguments') || - apiFunction.hasOwnProperty('caller') || - apiFunction.hasOwnProperty('prototype') || - apiFunction.hasOwnProperty('toString')), - ['failed descriptor keys']: (Object.keys(Object.getOwnPropertyDescriptors(apiFunction)).sort().toString() != 'length,name'), - ['failed own property names']: (Object.getOwnPropertyNames(apiFunction).sort().toString() != 'length,name'), - ['failed own keys names']: HAS_REFLECT && (Reflect.ownKeys(apiFunction).sort().toString() != 'length,name'), - // Proxy Detection - ['failed object toString error']: (failsTypeError({ - spawnErr: () => Object.create(apiFunction).toString(), - withStack: (err) => IS_BLINK && !hasValidStack(err, AT_FUNCTION), - }) || - failsTypeError({ - spawnErr: () => Object.create(new Proxy(apiFunction, {})).toString(), - withStack: (err) => IS_BLINK && !hasValidStack(err, AT_OBJECT), - })), - ['failed at incompatible proxy error']: failsTypeError({ - spawnErr: () => { - apiFunction.arguments; - apiFunction.caller; - }, - withStack: (err) => IS_GECKO && !hasValidStack(err, STRICT_MODE, 0), - }), - ['failed at toString incompatible proxy error']: failsTypeError({ - spawnErr: () => { - apiFunction.toString.arguments; - apiFunction.toString.caller; - }, - withStack: (err) => IS_GECKO && !hasValidStack(err, STRICT_MODE, 0), - }), - ['failed at too much recursion error']: failsTypeError({ - spawnErr: () => { - Object.setPrototypeOf(apiFunction, Object.create(apiFunction)).toString(); - }, - final: () => Object.setPrototypeOf(apiFunction, nativeProto), - }), - }; - // conditionally increase difficulty - const detectProxies = (name == 'toString' || - !!lieProps['Function.toString'] || - !!lieProps['Permissions.query']); - if (detectProxies) { - const proxy1 = new Proxy(apiFunction, {}); - const proxy2 = new Proxy(apiFunction, {}); - const proxy3 = new Proxy(apiFunction, {}); - lies = { - ...lies, - // Advanced Proxy Detection - ['failed at too much recursion __proto__ error']: !failsTypeError({ - spawnErr: () => { - // @ts-expect-error - apiFunction.__proto__ = proxy; - apiFunction++; - }, - final: () => Object.setPrototypeOf(apiFunction, nativeProto), - }), - ['failed at chain cycle error']: !failsTypeError({ - spawnErr: () => { - Object.setPrototypeOf(proxy1, Object.create(proxy1)).toString(); - }, - final: () => Object.setPrototypeOf(proxy1, nativeProto), - }), - ['failed at chain cycle __proto__ error']: !failsTypeError({ - spawnErr: () => { - // @ts-expect-error - proxy2.__proto__ = proxy2; - proxy2++; - }, - final: () => Object.setPrototypeOf(proxy2, nativeProto), - }), - ['failed at reflect set proto']: HAS_REFLECT && failsTypeError({ - spawnErr: () => { - Reflect.setPrototypeOf(apiFunction, Object.create(apiFunction)); - RAND in apiFunction; - throw new TypeError(); - }, - final: () => Object.setPrototypeOf(apiFunction, nativeProto), - }), - ['failed at reflect set proto proxy']: HAS_REFLECT && !failsTypeError({ - spawnErr: () => { - Reflect.setPrototypeOf(proxy3, Object.create(proxy3)); - RAND in proxy3; - }, - final: () => Object.setPrototypeOf(proxy3, nativeProto), - }), - ['failed at instanceof check error']: IS_BLINK && (failsTypeError({ - spawnErr: () => { - apiFunction instanceof apiFunction; - }, - withStack: (err) => !hasValidStack(err, FUNCTION_INSTANCE), - }) || - failsTypeError({ - spawnErr: () => { - const proxy = new Proxy(apiFunction, {}); - proxy instanceof proxy; - }, - withStack: (err) => !hasValidStack(err, PROXY_INSTANCE), - })), - ['failed at define properties']: IS_BLINK && HAS_REFLECT && failsWithError(() => { - Object.defineProperty(apiFunction, '', { configurable: true }).toString(); - Reflect.deleteProperty(apiFunction, ''); - }), - }; - } - const lieTypes = Object.keys(lies).filter((key) => !!lies[key]); - return { - lied: lieTypes.length, - lieTypes, - }; - } - function createLieDetector(scope) { - const isSupported = (obj) => typeof obj != 'undefined' && !!obj; - const props = {}; // lie list and detail - const propsSearched = []; // list of properties searched - return { - getProps: () => props, - getPropsSearched: () => propsSearched, - searchLies: (fn, config) => { - const { target, ignore } = config || {}; - let obj; - // check if api is blocked or not supported - try { - obj = fn(); - if (!isSupported(obj)) { - return; - } - } - catch (error) { - return; - } - const interfaceObject = !!obj.prototype ? obj.prototype : obj; - [...new Set([ - ...Object.getOwnPropertyNames(interfaceObject), - ...Object.keys(interfaceObject), // backup - ])].sort().forEach((name) => { - const skip = (name == 'constructor' || - (target && !new Set(target).has(name)) || - (ignore && new Set(ignore).has(name))); - if (skip) - return; - const objectNameString = /\s(.+)\]/; - const apiName = `${obj.name ? obj.name : - objectNameString.test(obj) ? objectNameString.exec(obj)?.[1] : - undefined}.${name}`; - propsSearched.push(apiName); - try { - const proto = obj.prototype ? obj.prototype : obj; - let res; // response from getLies - // search if function - try { - const apiFunction = proto[name]; // may trigger TypeError - if (typeof apiFunction == 'function') { - res = queryLies({ - scope, - apiFunction: proto[name], - proto, - obj: null, - lieProps: props, - }); - if (res.lied) { - documentLie(apiName, res.lieTypes); - return (props[apiName] = res.lieTypes); - } - return; - } - // since there is no TypeError and the typeof is not a function, - // handle invalid values and ignore name, length, and constants - if (name != 'name' && - name != 'length' && - name[0] !== name[0].toUpperCase()) { - const lie = ['failed descriptor.value undefined']; - documentLie(apiName, lie); - return (props[apiName] = lie); - } - } - catch (error) { } - // else search getter function - // @ts-ignore - const getterFunction = Object.getOwnPropertyDescriptor(proto, name).get; - res = queryLies({ - scope, - apiFunction: getterFunction, - proto, - obj, - lieProps: props, - }); // send the obj for special tests - if (res.lied) { - documentLie(apiName, res.lieTypes); - return (props[apiName] = res.lieTypes); - } - return; - } - catch (error) { - const lie = `failed prototype test execution`; - documentLie(apiName, lie); - return (props[apiName] = [lie]); - } - }); - }, - }; - } - function getPhantomIframe() { - if (IS_WORKER_SCOPE) - return { iframeWindow: self }; - try { - const numberOfIframes = self.length; - const frag = new DocumentFragment(); - const div = document.createElement('div'); - const id = getRandomValues(); - div.setAttribute('id', id); - frag.appendChild(div); - div.innerHTML = `
`; - document.body.appendChild(frag); - const iframeWindow = self[numberOfIframes]; - const phantomWindow = getBehemothIframe(iframeWindow); - return { iframeWindow: phantomWindow || self, div }; - } - catch (error) { - captureError(error, 'client blocked phantom iframe'); - return { iframeWindow: self }; - } - } - const { iframeWindow: PHANTOM_DARKNESS, div: PARENT_PHANTOM } = getPhantomIframe() || {}; - function getPrototypeLies(scope) { - const lieDetector = createLieDetector(scope); - const { searchLies, } = lieDetector; - // search lies: remove target to search all properties - // test Function.toString first to determine the depth of the search - searchLies(() => Function, { - target: [ - 'toString', - ], - ignore: [ - 'caller', - 'arguments', - ], - }); - // other APIs - searchLies(() => AnalyserNode); - searchLies(() => AudioBuffer, { - target: [ - 'copyFromChannel', - 'getChannelData', - ], - }); - searchLies(() => BiquadFilterNode, { - target: [ - 'getFrequencyResponse', - ], - }); - searchLies(() => CanvasRenderingContext2D, { - target: [ - 'getImageData', - 'getLineDash', - 'isPointInPath', - 'isPointInStroke', - 'measureText', - 'quadraticCurveTo', - 'fillText', - 'strokeText', - 'font', - ], - }); - searchLies(() => CSSStyleDeclaration, { - target: [ - 'setProperty', - ], - }); - // @ts-expect-error - searchLies(() => CSS2Properties, { - target: [ - 'setProperty', - ], - }); - searchLies(() => Date, { - target: [ - 'getDate', - 'getDay', - 'getFullYear', - 'getHours', - 'getMinutes', - 'getMonth', - 'getTime', - 'getTimezoneOffset', - 'setDate', - 'setFullYear', - 'setHours', - 'setMilliseconds', - 'setMonth', - 'setSeconds', - 'setTime', - 'toDateString', - 'toJSON', - 'toLocaleDateString', - 'toLocaleString', - 'toLocaleTimeString', - 'toString', - 'toTimeString', - 'valueOf', - ], - }); - // @ts-expect-error if not supported - searchLies(() => GPU, { - target: [ - 'requestAdapter', - ], - }); - // @ts-expect-error if not supported - searchLies(() => GPUAdapter, { - target: [ - 'requestAdapterInfo', - ], - }); - searchLies(() => Intl.DateTimeFormat, { - target: [ - 'format', - 'formatRange', - 'formatToParts', - 'resolvedOptions', - ], - }); - searchLies(() => Document, { - target: [ - 'createElement', - 'createElementNS', - 'getElementById', - 'getElementsByClassName', - 'getElementsByName', - 'getElementsByTagName', - 'getElementsByTagNameNS', - 'referrer', - 'write', - 'writeln', - ], - ignore: [ - // Gecko - 'onreadystatechange', - 'onmouseenter', - 'onmouseleave', - ], - }); - searchLies(() => DOMRect); - searchLies(() => DOMRectReadOnly); - searchLies(() => Element, { - target: [ - 'append', - 'appendChild', - 'getBoundingClientRect', - 'getClientRects', - 'insertAdjacentElement', - 'insertAdjacentHTML', - 'insertAdjacentText', - 'insertBefore', - 'prepend', - 'replaceChild', - 'replaceWith', - 'setAttribute', - ], - }); - searchLies(() => FontFace, { - target: [ - 'family', - 'load', - 'status', - ], - }); - searchLies(() => HTMLCanvasElement); - searchLies(() => HTMLElement, { - target: [ - 'clientHeight', - 'clientWidth', - 'offsetHeight', - 'offsetWidth', - 'scrollHeight', - 'scrollWidth', - ], - ignore: [ - // Gecko - 'onmouseenter', - 'onmouseleave', - ], - }); - searchLies(() => HTMLIFrameElement, { - target: [ - 'contentDocument', - 'contentWindow', - ], - }); - searchLies(() => IntersectionObserverEntry, { - target: [ - 'boundingClientRect', - 'intersectionRect', - 'rootBounds', - ], - }); - searchLies(() => Math, { - target: [ - 'acos', - 'acosh', - 'asinh', - 'atan', - 'atan2', - 'atanh', - 'cbrt', - 'cos', - 'cosh', - 'exp', - 'expm1', - 'log', - 'log10', - 'log1p', - 'sin', - 'sinh', - 'sqrt', - 'tan', - 'tanh', - ], - }); - searchLies(() => MediaDevices, { - target: [ - 'enumerateDevices', - 'getDisplayMedia', - 'getUserMedia', - ], - }); - searchLies(() => Navigator, { - target: [ - 'appCodeName', - 'appName', - 'appVersion', - 'buildID', - 'connection', - 'deviceMemory', - 'getBattery', - 'getGamepads', - 'getVRDisplays', - 'hardwareConcurrency', - 'language', - 'languages', - 'maxTouchPoints', - 'mimeTypes', - 'oscpu', - 'platform', - 'plugins', - 'product', - 'productSub', - 'sendBeacon', - 'serviceWorker', - 'storage', - 'userAgent', - 'vendor', - 'vendorSub', - 'webdriver', - 'gpu', - ], - }); - searchLies(() => Node, { - target: [ - 'appendChild', - 'insertBefore', - 'replaceChild', - ], - }); - // @ts-expect-error - searchLies(() => OffscreenCanvas, { - target: [ - 'convertToBlob', - 'getContext', - ], - }); - // @ts-expect-error - searchLies(() => OffscreenCanvasRenderingContext2D, { - target: [ - 'getImageData', - 'getLineDash', - 'isPointInPath', - 'isPointInStroke', - 'measureText', - 'quadraticCurveTo', - 'font', - ], - }); - searchLies(() => Permissions, { - target: [ - 'query', - ], - }); - searchLies(() => Range, { - target: [ - 'getBoundingClientRect', - 'getClientRects', - ], - }); - // @ts-expect-error - searchLies(() => Intl.RelativeTimeFormat, { - target: [ - 'resolvedOptions', - ], - }); - searchLies(() => Screen); - searchLies(() => speechSynthesis, { - target: [ - 'getVoices', - ], - }); - searchLies(() => String, { - target: [ - 'fromCodePoint', - ], - }); - searchLies(() => StorageManager, { - target: [ - 'estimate', - ], - }); - searchLies(() => SVGRect); - searchLies(() => SVGRectElement, { - target: [ - 'getBBox', - ], - }); - searchLies(() => SVGTextContentElement, { - target: [ - 'getExtentOfChar', - 'getSubStringLength', - 'getComputedTextLength', - ], - }); - searchLies(() => TextMetrics); - searchLies(() => WebGLRenderingContext, { - target: [ - 'bufferData', - 'getParameter', - 'readPixels', - ], - }); - searchLies(() => WebGL2RenderingContext, { - target: [ - 'bufferData', - 'getParameter', - 'readPixels', - ], - }); - /* potential targets: - RTCPeerConnection - Plugin - PluginArray - MimeType - MimeTypeArray - Worker - History - */ - // return lies list and detail - const props = lieDetector.getProps(); - const propsSearched = lieDetector.getPropsSearched(); - return { - lieDetector, - lieList: Object.keys(props).sort(), - lieDetail: props, - lieCount: Object.keys(props).reduce((acc, key) => acc + props[key].length, 0), - propsSearched, - }; - } - // start program - const start = performance.now(); - const { lieDetector, lieList, lieDetail, - // lieCount, - propsSearched, } = getPrototypeLies(PHANTOM_DARKNESS); // execute and destructure the list and detail - // disregard Function.prototype.toString lies when determining if the API can be trusted - const getNonFunctionToStringLies = (x) => !x ? x : x.filter((x) => !/object toString|toString incompatible proxy/.test(x)).length; - let lieProps; - let prototypeLies; - let PROTO_BENCHMARK = 0; - if (!IS_WORKER_SCOPE) { - lieProps = (() => { - const props = lieDetector.getProps(); - return Object.keys(props).reduce((acc, key) => { - acc[key] = getNonFunctionToStringLies(props[key]); - return acc; - }, {}); - })(); - prototypeLies = JSON.parse(JSON.stringify(lieDetail)); - const perf = performance.now() - start; - PROTO_BENCHMARK = +perf.toFixed(2); - const message = `${propsSearched.length} API properties analyzed in ${PROTO_BENCHMARK}ms (${lieList.length} corrupted)`; - setTimeout(() => console.log(message), 3000); - } - const getPluginLies = (plugins, mimeTypes) => { - const lies = []; // collect lie types - const pluginsOwnPropertyNames = Object.getOwnPropertyNames(plugins).filter((name) => isNaN(+name)); - const mimeTypesOwnPropertyNames = Object.getOwnPropertyNames(mimeTypes).filter((name) => isNaN(+name)); - // cast to array - const pluginsList = [...plugins]; - const mimeTypesList = [...mimeTypes]; - // get initial trusted mimeType names - const trustedMimeTypes = new Set(mimeTypesOwnPropertyNames); - // get initial trusted plugin names - const excludeDuplicates = (arr) => [...new Set(arr)]; - const mimeTypeEnabledPlugins = excludeDuplicates(mimeTypesList.map((mimeType) => mimeType.enabledPlugin)); - const trustedPluginNames = new Set(pluginsOwnPropertyNames); - const mimeTypeEnabledPluginsNames = mimeTypeEnabledPlugins.map((plugin) => plugin && plugin.name); - const trustedPluginNamesArray = [...trustedPluginNames]; - trustedPluginNamesArray.forEach((name) => { - const validName = new Set(mimeTypeEnabledPluginsNames).has(name); - if (!validName) { - trustedPluginNames.delete(name); - } - }); - // 3. Expect MimeType object in plugins - const invalidPlugins = pluginsList.filter((plugin) => { - try { - const validMimeType = Object.getPrototypeOf(plugin[0]).constructor.name == 'MimeType'; - if (!validMimeType) { - trustedPluginNames.delete(plugin.name); - } - return !validMimeType; - } - catch (error) { - trustedPluginNames.delete(plugin.name); - return true; // sign of tampering - } - }); - if (invalidPlugins.length) { - lies.push('missing mimetype'); - } - // 4. Expect valid MimeType(s) in plugin - const pluginMimeTypes = pluginsList - .map((plugin) => Object.values(plugin)).flat(); - const pluginMimeTypesNames = pluginMimeTypes.map((mimetype) => mimetype.type); - pluginMimeTypesNames.forEach((name) => { - const validName = trustedMimeTypes.has(name); - if (!validName) { - trustedMimeTypes.delete(name); - } - }); - pluginsList.forEach((plugin) => { - const pluginMimeTypes = Object.values(plugin).map((mimetype) => mimetype.type); - return pluginMimeTypes.forEach((mimetype) => { - if (!trustedMimeTypes.has(mimetype)) { - lies.push('invalid mimetype'); - return trustedPluginNames.delete(plugin.name); - } - return; - }); - }); - return { - validPlugins: pluginsList.filter((plugin) => trustedPluginNames.has(plugin.name)), - validMimeTypes: mimeTypesList.filter((mimeType) => trustedMimeTypes.has(mimeType.type)), - lies: [...new Set(lies)], // remove duplicates - }; - }; - const getLies = () => { - const records = lieRecords.getRecords(); - const totalLies = Object.keys(records).reduce((acc, key) => { - acc += records[key].length; - return acc; - }, 0); - return { data: records, totalLies }; - }; - - // Detect proxy behavior - const proxyBehavior = (x) => typeof x == 'function' ? true : false; - const GIBBERS = /[cC]f|[jJ][bcdfghlmprsty]|[qQ][bcdfghjklmnpsty]|[vV][bfhjkmpt]|[xX][dkrz]|[yY]y|[zZ][fr]|[cCxXzZ]j|[bBfFgGjJkKpPvVqQtTwWyYzZ]q|[cCfFgGjJpPqQwW]v|[jJqQvV]w|[bBcCdDfFgGhHjJkKmMpPqQsSvVwWxXzZ]x|[bBfFhHjJkKmMpPqQ]z/g; - // Detect gibberish - const gibberish = (str, { strict = false } = {}) => { - if (!str) - return []; - // test letter case sequence - const letterCaseSequenceGibbers = []; - const tests = [ - /([A-Z]{3,}[a-z])/g, // ABCd - /([a-z][A-Z]{3,})/g, // aBCD - /([a-z][A-Z]{2,}[a-z])/g, // aBC...z - /([a-z][\d]{2,}[a-z])/g, // a##...b - /([A-Z][\d]{2,}[a-z])/g, // A##...b - /([a-z][\d]{2,}[A-Z])/g, // a##...B - ]; - tests.forEach((regExp) => { - const match = str.match(regExp); - if (match) { - return letterCaseSequenceGibbers.push(match.join(', ')); - } - return; - }); - // test letter sequence - const letterSequenceGibbers = []; - const clean = str.replace(/\d|\W|_/g, ' ').replace(/\s+/g, ' ').trim().split(' ').join('_'); - const len = clean.length; - const arr = [...clean]; - arr.forEach((char, index) => { - const nextIndex = index + 1; - const nextChar = arr[nextIndex]; - const isWordSequence = nextChar !== '_' && char !== '_' && nextIndex !== len; - if (isWordSequence) { - const combo = char + nextChar; - if (GIBBERS.test(combo)) - letterSequenceGibbers.push(combo); - } - }); - const gibbers = [ - // ignore sequence if less than 3 exist - ...(!strict && (letterSequenceGibbers.length < 3) ? [] : letterSequenceGibbers), - ...(!strict && (letterCaseSequenceGibbers.length < 4) ? [] : letterCaseSequenceGibbers), - ]; - const allow = [ - // known gibbers - 'bz', - 'cf', - 'fx', - 'mx', - 'vb', - 'xd', - 'gx', - 'PCIe', - 'vm', - 'NVIDIAGa', - ]; - return gibbers.filter((x) => !allow.includes(x)); - }; - // WebGL Renderer helpers - function compressWebGLRenderer(x) { - if (!x) - return; - return ('' + x) - .replace(/ANGLE \(|\sDirect3D.+|\sD3D.+|\svs_.+\)|\((DRM|POLARIS|LLVM).+|Mesa.+|(ATI|INTEL)-.+|Metal\s-\s.+|NVIDIA\s[\d|\.]+/ig, '') - .replace(/(\s(ti|\d{1,2}GB|super)$)/ig, '') - .replace(/\s{2,}/g, ' ') - .trim() - .replace(/((r|g)(t|)(x|s|\d) |Graphics |GeForce |Radeon (HD |Pro |))(\d+)/i, (...args) => { - return `${args[1]}${args[6][0]}${args[6].slice(1).replace(/\d/g, '0')}s`; - }); - } - const getWebGLRendererParts = (x) => { - const knownParts = [ - 'AMD', - 'ANGLE', - 'ASUS', - 'ATI', - 'ATI Radeon', - 'ATI Technologies Inc', - 'Adreno', - 'Android Emulator', - 'Apple', - 'Apple GPU', - 'Apple M1', - 'Chipset', - 'D3D11', - 'Direct3D', - 'Express Chipset', - 'GeForce', - 'Generation', - 'Generic Renderer', - 'Google', - 'Google SwiftShader', - 'Graphics', - 'Graphics Media Accelerator', - 'HD Graphics Family', - 'Intel', - 'Intel(R) HD Graphics', - 'Intel(R) UHD Graphics', - 'Iris', - 'KBL Graphics', - 'Mali', - 'Mesa', - 'Mesa DRI', - 'Metal', - 'Microsoft', - 'Microsoft Basic Render Driver', - 'Microsoft Corporation', - 'NVIDIA', - 'NVIDIA Corporation', - 'NVIDIAGameReadyD3D', - 'OpenGL', - 'OpenGL Engine', - 'Open Source Technology Center', - 'Parallels', - 'Parallels Display Adapter', - 'PCIe', - 'Plus Graphics', - 'PowerVR', - 'Pro Graphics', - 'Quadro', - 'Radeon', - 'Radeon Pro', - 'Radeon Pro Vega', - 'Samsung', - 'SSE2', - 'VMware', - 'VMware SVGA 3D', - 'Vega', - 'VirtualBox', - 'VirtualBox Graphics Adapter', - 'Vulkan', - 'Xe Graphics', - 'llvmpipe', - ]; - const parts = [...knownParts].filter((name) => ('' + x).includes(name)); - return [...new Set(parts)].sort().join(', '); - }; - const getWebGLRendererConfidence = (x) => { - if (!x) { - return; - } - const parts = getWebGLRendererParts(x); - const hasKnownParts = parts.length; - const hasBlankSpaceNoise = /\s{2,}|^\s|\s$/.test(x); - const hasBrokenAngleStructure = /^ANGLE/.test(x) && !(/^ANGLE \((.+)\)/.exec(x) || [])[1]; - // https://chromium.googlesource.com/angle/angle/+/83fa18905d8fed4f394e4f30140a83a3e76b1577/src/gpu_info_util/SystemInfo.cpp - // https://chromium.googlesource.com/angle/angle/+/83fa18905d8fed4f394e4f30140a83a3e76b1577/src/gpu_info_util/SystemInfo.h - // https://chromium.googlesource.com/chromium/src/+/refs/heads/main/ui/gl/gl_version_info.cc - /* - const knownVendors = [ - 'AMD', - 'ARM', - 'Broadcom', - 'Google', - 'ImgTec', - 'Intel', - 'Kazan', - 'NVIDIA', - 'Qualcomm', - 'VeriSilicon', - 'Vivante', - 'VMWare', - 'Apple', - 'Unknown' - ] - const angle = { - vendorId: (/^ANGLE \(([^,]+),/.exec(x)||[])[1] || knownVendors.find(vendor => x.includes(vendor)), - deviceId: ( - (x.match(/,/g)||[]).length == 2 ? (/^ANGLE \(([^,]+), ([^,]+)[,|\)]/.exec(x)||[])[2] : - (/^ANGLE \(([^,]+), ([^,]+)[,|\)]/.exec(x)||[])[1] || (/^ANGLE \((.+)\)$/.exec(x)||[])[1] - ).replace(/\sDirect3D.+/, '') - } - */ - const gibbers = gibberish(x, { strict: true }).join(', '); - const valid = (hasKnownParts && !hasBlankSpaceNoise && !hasBrokenAngleStructure); - const confidence = (valid && !gibbers.length ? 'high' : - valid && gibbers.length ? 'moderate' : - 'low'); - const grade = (confidence == 'high' ? 'A' : - confidence == 'moderate' ? 'C' : - 'F'); - const warnings = new Set([ - (hasBlankSpaceNoise ? 'found extra spaces' : undefined), - (hasBrokenAngleStructure ? 'broken angle structure' : undefined), - ]); - warnings.delete(undefined); - return { - parts, - warnings: [...warnings], - gibbers, - confidence, - grade, - }; - }; - // Collect trash values - const createTrashBin = () => { - const bin = []; - return { - getBin: () => bin, - sendToTrash: (name, val, response = undefined) => { - const proxyLike = proxyBehavior(val); - const value = !proxyLike ? val : 'proxy behavior detected'; - bin.push({ name, value }); - return response; - }, - }; - }; - const trashBin = createTrashBin(); - const { sendToTrash } = trashBin; - const getTrash = () => ({ trashBin: trashBin.getBin() }); - - function isFontOSBad(userAgentOS, fonts) { - if (!userAgentOS || !fonts || !fonts.length) - return false; - const fontMap = fonts.reduce((acc, x) => { - acc[x] = true; - return acc; - }, {}); - const isLikeWindows = ('Cambria Math' in fontMap || - 'Nirmala UI' in fontMap || - 'Leelawadee UI' in fontMap || - 'HoloLens MDL2 Assets' in fontMap || - 'Segoe Fluent Icons' in fontMap); - const isLikeApple = ('Helvetica Neue' in fontMap || - 'Luminari' in fontMap || - 'PingFang HK Light' in fontMap || - 'InaiMathi Bold' in fontMap || - 'Galvji' in fontMap || - 'Chakra Petch' in fontMap); - const isLikeLinux = ('Arimo' in fontMap || - 'MONO' in fontMap || - 'Ubuntu' in fontMap || - 'Noto Color Emoji' in fontMap || - 'Dancing Script' in fontMap || - 'Droid Sans Mono' in fontMap); - if (isLikeWindows && userAgentOS != "Windows" /* PlatformClassifier.WINDOWS */) { - return true; - } - else if (isLikeApple && userAgentOS != "Apple" /* PlatformClassifier.APPLE */) { - return true; - } - else if (isLikeLinux && userAgentOS != "Linux" /* PlatformClassifier.LINUX */) { - return true; - } - return false; - } - - let WORKER_TYPE = ''; - let WORKER_NAME = ''; - async function spawnWorker() { - const ask = (fn) => { - try { - return fn(); - } - catch (e) { - return; - } - }; - function getWorkerPrototypeLies(scope) { - const lieDetector = createLieDetector(scope); - const { searchLies, } = lieDetector; - searchLies(() => Function, { - target: [ - 'toString', - ], - ignore: [ - 'caller', - 'arguments', - ], - }); - // @ts-expect-error - searchLies(() => WorkerNavigator, { - target: [ - 'deviceMemory', - 'hardwareConcurrency', - 'language', - 'languages', - 'platform', - 'userAgent', - ], - }); - // return lies list and detail - const props = lieDetector.getProps(); - const propsSearched = lieDetector.getPropsSearched(); - return { - lieDetector, - lieList: Object.keys(props).sort(), - lieDetail: props, - lieCount: Object.keys(props).reduce((acc, key) => acc + props[key].length, 0), - propsSearched, - }; - } - const getUserAgentData = async (navigator) => { - if (!('userAgentData' in navigator)) { - return; - } - const data = await navigator.userAgentData.getHighEntropyValues(['platform', 'platformVersion', 'architecture', 'bitness', 'model', 'uaFullVersion']); - const { brands, mobile } = navigator.userAgentData || {}; - const compressedBrands = (brands, captureVersion = false) => brands - .filter((obj) => !/Not/.test(obj.brand)).map((obj) => `${obj.brand}${captureVersion ? ` ${obj.version}` : ''}`); - const removeChromium = (brands) => (brands.length > 1 ? brands.filter((brand) => !/Chromium/.test(brand)) : brands); - // compress brands - if (!data.brands) { - data.brands = brands; - } - data.brandsVersion = compressedBrands(data.brands, true); - data.brands = compressedBrands(data.brands); - data.brandsVersion = removeChromium(data.brandsVersion); - data.brands = removeChromium(data.brands); - if (!data.mobile) { - data.mobile = mobile; - } - const dataSorted = Object.keys(data).sort().reduce((acc, key) => { - acc[key] = data[key]; - return acc; - }, {}); - return dataSorted; - }; - const getWebglData = () => ask(() => { - // @ts-ignore - const canvasOffscreenWebgl = new OffscreenCanvas(256, 256); - const contextWebgl = canvasOffscreenWebgl.getContext('webgl'); - const rendererInfo = contextWebgl.getExtension('WEBGL_debug_renderer_info'); - return { - webglVendor: contextWebgl.getParameter(rendererInfo.UNMASKED_VENDOR_WEBGL), - webglRenderer: contextWebgl.getParameter(rendererInfo.UNMASKED_RENDERER_WEBGL), - }; - }); - const computeTimezoneOffset = () => { - const date = new Date().getDate(); - const month = new Date().getMonth(); - // @ts-ignore - const year = Date().split ` `[3]; // current year - const format = (n) => ('' + n).length == 1 ? `0${n}` : n; - const dateString = `${month + 1}/${format(date)}/${year}`; - const dateStringUTC = `${year}-${format(month + 1)}-${format(date)}`; - // @ts-ignore - const utc = Date.parse(new Date(dateString)); - const now = +new Date(dateStringUTC); - return +(((utc - now) / 60000).toFixed(0)); - }; - const getLocale = () => { - const constructors = [ - 'Collator', - 'DateTimeFormat', - 'DisplayNames', - 'ListFormat', - 'NumberFormat', - 'PluralRules', - 'RelativeTimeFormat', - ]; - // @ts-ignore - const locale = constructors.reduce((acc, name) => { - try { - const obj = new Intl[name]; - if (!obj) { - return acc; - } - const { locale } = obj.resolvedOptions() || {}; - return [...acc, locale]; - } - catch (error) { - return acc; - } - }, []); - return [...new Set(locale)]; - }; - const getWorkerData = async () => { - const timer = createTimer(); - await queueEvent(timer); - const userAgentData = await getUserAgentData(navigator).catch((error) => console.error(error)); - // webgl - const { webglVendor, webglRenderer } = getWebglData() || {}; - // timezone & locale - const timezoneOffset = computeTimezoneOffset(); - // eslint-disable-next-line new-cap - const timezoneLocation = Intl.DateTimeFormat().resolvedOptions().timeZone; - const locale = getLocale(); - // navigator - const { hardwareConcurrency, language, languages, platform, userAgent, - // @ts-expect-error - deviceMemory, } = navigator || {}; - // prototype lies - await queueEvent(timer); - const { - // lieDetector: lieProps, - lieList, lieDetail, - // lieCount, - // propsSearched, - } = getWorkerPrototypeLies(self); // execute and destructure the list and detail - // const prototypeLies = JSON.parse(JSON.stringify(lieDetail)) - const protoLieLen = lieList.length; - // match engine locale to system locale to determine if locale entropy is trusty - let systemCurrencyLocale; - const lang = ('' + language).split(',')[0]; - try { - systemCurrencyLocale = (1).toLocaleString((lang || undefined), { - style: 'currency', - currency: 'USD', - currencyDisplay: 'name', - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }); - } - catch (e) { } - const engineCurrencyLocale = (1).toLocaleString(undefined, { - style: 'currency', - currency: 'USD', - currencyDisplay: 'name', - minimumFractionDigits: 0, - maximumFractionDigits: 0, - }); - const localeEntropyIsTrusty = engineCurrencyLocale == systemCurrencyLocale; - const localeIntlEntropyIsTrusty = new Set(('' + language).split(',')).has('' + locale); - const { href, pathname } = self.location || {}; - const locationPathNameLie = (!href || - !pathname || - !/^\/(docs|creepjs|public)|\/creep.js$/.test(pathname) || - !new RegExp(`${pathname}$`).test(href)); - return { - lied: protoLieLen || +locationPathNameLie, - lies: { - proto: protoLieLen ? lieDetail : false, - }, - locale: '' + locale, - systemCurrencyLocale, - engineCurrencyLocale, - localeEntropyIsTrusty, - localeIntlEntropyIsTrusty, - timezoneOffset, - timezoneLocation, - deviceMemory, - hardwareConcurrency, - language, - languages: '' + languages, - platform, - userAgent, - webglRenderer, - webglVendor, - userAgentData, - }; - }; - // Compute and communicate from worker scope - const onEvent = (eventType, fn) => addEventListener(eventType, fn); - const send = (source) => { - return getWorkerData().then((data) => source.postMessage(data)); - }; - if (IS_WORKER_SCOPE) { - globalThis.ServiceWorkerGlobalScope ? onEvent('message', (e) => send(e.source)) : - globalThis.SharedWorkerGlobalScope ? onEvent('connect', (e) => send(e.ports[0])) : - send(self); // DedicatedWorkerGlobalScope - } - return IS_WORKER_SCOPE ? 0 /* Scope.WORKER */ : 1 /* Scope.WINDOW */; - } - async function getBestWorkerScope() { - try { - const timer = createTimer(); - await queueEvent(timer); - const ask = (fn) => { - try { - return fn(); - } - catch (e) { - return; - } - }; - const hasConstructor = (x, name) => x && x.__proto__.constructor.name == name; - const getDedicatedWorker = ({ scriptSource }) => new Promise((resolve) => { - const giveUpOnWorker = setTimeout(() => { - return resolve(null); - }, 3000); - const dedicatedWorker = ask(() => new Worker(scriptSource)); - if (!hasConstructor(dedicatedWorker, 'Worker')) - return resolve(null); - dedicatedWorker.onmessage = (event) => { - dedicatedWorker.terminate(); - clearTimeout(giveUpOnWorker); - return resolve(event.data); - }; - }); - const getSharedWorker = ({ scriptSource }) => new Promise((resolve) => { - const giveUpOnWorker = setTimeout(() => { - return resolve(null); - }, 3000); - const sharedWorker = ask(() => new SharedWorker(scriptSource)); - if (!hasConstructor(sharedWorker, 'SharedWorker')) - return resolve(null); - sharedWorker.port.start(); - sharedWorker.port.onmessage = (event) => { - sharedWorker.port.close(); - clearTimeout(giveUpOnWorker); - return resolve(event.data); - }; - }); - const getServiceWorker = ({ scriptSource }) => new Promise((resolve) => { - const giveUpOnWorker = setTimeout(() => { - return resolve(null); - }, 4000); - if (!ask(() => navigator.serviceWorker.register)) - return resolve(null); - return navigator.serviceWorker.register(scriptSource).then((registration) => { - if (!hasConstructor(registration, 'ServiceWorkerRegistration')) - return resolve(null); - return navigator.serviceWorker.ready.then((registration) => { - // @ts-ignore - registration.active.postMessage(undefined); - navigator.serviceWorker.onmessage = (event) => { - registration.unregister(); - clearTimeout(giveUpOnWorker); - return resolve(event.data); - }; - }); - }).catch((error) => { - console.error(error); - clearTimeout(giveUpOnWorker); - return resolve(null); - }); - }); - const scriptSource = './creep.js'; - WORKER_NAME = 'ServiceWorkerGlobalScope'; - WORKER_TYPE = 'service'; // loads fast but is not available in frames - let workerScope = await getServiceWorker({ scriptSource }).catch((error) => { - captureError(error); - console.error(error.message); - return; - }); - if (!(workerScope || {}).userAgent) { - WORKER_NAME = 'SharedWorkerGlobalScope'; - WORKER_TYPE = 'shared'; // no support in Safari, iOS, and Chrome Android - workerScope = await getSharedWorker({ scriptSource }).catch((error) => { - captureError(error); - console.error(error.message); - return; - }); - } - if (!(workerScope || {}).userAgent) { - WORKER_NAME = 'DedicatedWorkerGlobalScope'; - WORKER_TYPE = 'dedicated'; // device emulators can easily spoof dedicated scope - workerScope = await getDedicatedWorker({ scriptSource }).catch((error) => { - captureError(error); - console.error(error.message); - return; - }); - } - if (!(workerScope || {}).userAgent) { - return; - } - workerScope.system = getOS(workerScope.userAgent); - workerScope.device = getUserAgentPlatform({ userAgent: workerScope.userAgent }); - // detect lies - const { system, userAgent, userAgentData, platform, deviceMemory, hardwareConcurrency, } = workerScope || {}; - // navigator lies - // skip language and languages to respect valid engine language switching bug in Chrome - // these are more likely navigator lies, so don't trigger lied worker scope - const workerScopeMatchLie = 'does not match worker scope'; - if (platform != navigator.platform) { - documentLie('Navigator.platform', workerScopeMatchLie); - } - if (userAgent != navigator.userAgent) { - documentLie('Navigator.userAgent', workerScopeMatchLie); - } - if (hardwareConcurrency && (hardwareConcurrency != navigator.hardwareConcurrency)) { - documentLie('Navigator.hardwareConcurrency', workerScopeMatchLie); - } - // @ts-ignore - if (deviceMemory && (deviceMemory != navigator.deviceMemory)) { - documentLie('Navigator.deviceMemory', workerScopeMatchLie); - } - // prototype lies - if (workerScope.lies.proto) { - const { proto } = workerScope.lies; - const keys = Object.keys(proto); - keys.forEach((key) => { - const api = `WorkerGlobalScope.${key}`; - const lies = proto[key]; - lies.forEach((lie) => documentLie(api, lie)); - }); - } - // user agent os lie - const [userAgentOS, platformOS] = getReportedPlatform(userAgent, platform); - if (userAgentOS != platformOS) { - workerScope.lied = true; - workerScope.lies.os = `${platformOS} platform and ${userAgentOS} user agent do not match`; - documentLie('WorkerGlobalScope', workerScope.lies.os); - } - // user agent engine lie - const decryptedName = decryptUserAgent({ - ua: userAgent, - os: system, - isBrave: false, // default false since we are only looking for JS runtime and version - }); - const userAgentEngine = ((/safari/i.test(decryptedName) || /iphone|ipad/i.test(userAgent)) ? 'JavaScriptCore' : - /firefox/i.test(userAgent) ? 'SpiderMonkey' : - /chrome/i.test(userAgent) ? 'V8' : - undefined); - if (userAgentEngine != JS_ENGINE) { - workerScope.lied = true; - workerScope.lies.engine = `${JS_ENGINE} JS runtime and ${userAgentEngine} user agent do not match`; - documentLie('WorkerGlobalScope', workerScope.lies.engine); - } - // user agent version lie - const getVersion = (x) => (/\d+/.exec(x) || [])[0]; - const userAgentVersion = getVersion(decryptedName); - const userAgentDataVersion = getVersion(userAgentData ? userAgentData.uaFullVersion : ''); - const versionSupported = userAgentDataVersion && userAgentVersion; - const versionMatch = userAgentDataVersion == userAgentVersion; - if (versionSupported && !versionMatch) { - workerScope.lied = true; - workerScope.lies.version = `userAgentData version ${userAgentDataVersion} and user agent version ${userAgentVersion} do not match`; - documentLie('WorkerGlobalScope', workerScope.lies.version); - } - // platformVersion lie - const FEATURE_CASE = IS_BLINK && CSS.supports('accent-color: initial'); - const getPlatformVersionLie = (device, userAgentData) => { - if (!/windows|mac/i.test(device) || !userAgentData?.platformVersion) { - return false; - } - if (userAgentData.platform == 'macOS') { - return FEATURE_CASE ? /_/.test(userAgentData.platformVersion) : false; - } - const reportedVersionNumber = (/windows ([\d|\.]+)/i.exec(device) || [])[1]; - const windows10OrHigherReport = +reportedVersionNumber == 10; - const { platformVersion } = userAgentData; - const versionMap = { - '6.1': '7', - '6.2': '8', - '6.3': '8.1', - '10.0': '10', - }; - const version = versionMap[platformVersion]; - if (!FEATURE_CASE && version) { - return version != reportedVersionNumber; - } - const parts = platformVersion.split('.'); - if (parts.length != 3) - return true; - const windows10OrHigherPlatform = +parts[0] > 0; - return ((windows10OrHigherPlatform && !windows10OrHigherReport) || - (!windows10OrHigherPlatform && windows10OrHigherReport)); - }; - const windowsVersionLie = getPlatformVersionLie(workerScope.device, userAgentData); - if (windowsVersionLie) { - workerScope.lied = true; - workerScope.lies.platformVersion = `platform version is fake`; - documentLie('WorkerGlobalScope', workerScope.lies.platformVersion); - } - // capture userAgent version - workerScope.userAgentVersion = userAgentVersion; - workerScope.userAgentDataVersion = userAgentDataVersion; - workerScope.userAgentEngine = userAgentEngine; - const gpu = { - ...(getWebGLRendererConfidence(workerScope.webglRenderer) || {}), - compressedGPU: compressWebGLRenderer(workerScope.webglRenderer), - }; - logTestResult({ time: timer.stop(), test: `${WORKER_TYPE} worker`, passed: true }); - return { - ...workerScope, - gpu, - uaPostReduction: isUAPostReduction(workerScope.userAgent), - }; - } - catch (error) { - logTestResult({ test: 'worker', passed: false }); - captureError(error, 'workers failed or blocked by client'); - return; - } - } - function workerScopeHTML(fp) { - if (!fp.workerScope) { - return ` -
- Worker -
lang/timezone:
-
${HTMLNote.BLOCKED}
-
gpu:
-
${HTMLNote.BLOCKED}
-
-
-
userAgent:
-
${HTMLNote.BLOCKED}
-
device:
-
${HTMLNote.BLOCKED}
-
userAgentData:
-
${HTMLNote.BLOCKED}
-
`; - } - const { workerScope: data } = fp; - const { lied, locale, systemCurrencyLocale, engineCurrencyLocale, localeEntropyIsTrusty, localeIntlEntropyIsTrusty, timezoneOffset, timezoneLocation, deviceMemory, hardwareConcurrency, language, - // languages, - platform, userAgent, uaPostReduction, webglRenderer, webglVendor, gpu, userAgentData, system, device, $hash, } = data || {}; - const { parts, warnings, gibbers, confidence, grade: confidenceGrade, compressedGPU, } = gpu || {}; - return ` - ${performanceLogger.getLog()[`${WORKER_TYPE} worker`]} - ${WORKER_NAME || ''} - -
- - Worker${hashSlice($hash)} -
lang/timezone:
-
- ${localeEntropyIsTrusty ? `${language} (${systemCurrencyLocale})` : - `${language} (${engineCurrencyLocale})`} - ${locale === language ? '' : localeIntlEntropyIsTrusty ? ` ${locale}` : - ` ${locale}`} -
${timezoneLocation} (${'' + timezoneOffset}) -
- -
${confidence ? `confidence: ${confidence}` : ''}gpu:
-
- ${webglVendor ? webglVendor : ''} - ${webglRenderer ? `
${webglRenderer}` : HTMLNote.UNSUPPORTED} -
- -
-
- -
userAgent:${!uaPostReduction ? '' : `ua reduction`}
-
-
${userAgent || HTMLNote.UNSUPPORTED}
-
- -
device:
-
- ${`${system}${platform ? ` (${platform})` : ''}`} - ${device ? `
${device}` : HTMLNote.BLOCKED} - ${hardwareConcurrency && deviceMemory ? `
cores: ${hardwareConcurrency}, ram: ${deviceMemory}` : - hardwareConcurrency && !deviceMemory ? `
cores: ${hardwareConcurrency}` : - !hardwareConcurrency && deviceMemory ? `
ram: ${deviceMemory}` : ''} -
- -
userAgentData:
-
-
- ${((userAgentData) => { - const { architecture, bitness, brandsVersion, uaFullVersion, mobile, model, platformVersion, platform, } = userAgentData || {}; - // @ts-ignore - const windowsRelease = computeWindowsRelease({ platform, platformVersion }); - return !userAgentData ? HTMLNote.UNSUPPORTED : ` - ${(brandsVersion || []).join(',')}${uaFullVersion ? ` (${uaFullVersion})` : ''} -
${windowsRelease || `${platform} ${platformVersion}`} ${architecture ? `${architecture}${bitness ? `_${bitness}` : ''}` : ''} - ${model ? `
${model}` : ''} - ${mobile ? '
mobile' : ''} - `; - })(userAgentData)} -
-
- -
- `; - } - - // https://stackoverflow.com/a/22429679 - const hashMini = (x) => { - const json = `${JSON.stringify(x)}`; - const hash = json.split('').reduce((hash, char, i) => { - return Math.imul(31, hash) + json.charCodeAt(i) | 0; - }, 0x811c9dc5); - return ('0000000' + (hash >>> 0).toString(16)).substr(-8); - }; - // instance id - const instanceId = (String.fromCharCode(Math.random() * 26 + 97) + - Math.random().toString(36).slice(-7)); - // https://stackoverflow.com/a/53490958 - // https://stackoverflow.com/a/43383990 - // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest - const hashify = (x, algorithm = 'SHA-256') => { - const json = `${JSON.stringify(x)}`; - const jsonBuffer = new TextEncoder().encode(json); - return crypto.subtle.digest(algorithm, jsonBuffer).then((hashBuffer) => { - const hashArray = Array.from(new Uint8Array(hashBuffer)); - const hashHex = hashArray.map((b) => ('00' + b.toString(16)).slice(-2)).join(''); - return hashHex; - }); - }; - const getFuzzyHash = async (fp) => { - // requires update log (below) when adding new keys to fp - const metricKeys = [ - 'canvas2d.dataURI', - 'canvas2d.emojiSet', - 'canvas2d.emojiURI', - 'canvas2d.liedTextMetrics', - 'canvas2d.mods', - 'canvas2d.paintURI', - 'canvas2d.paintCpuURI', - 'canvas2d.textMetricsSystemSum', - 'canvas2d.textURI', - 'canvasWebgl.dataURI', - 'canvasWebgl.dataURI2', - 'canvasWebgl.extensions', - 'canvasWebgl.gpu', - 'canvasWebgl.parameterOrExtensionLie', - 'canvasWebgl.parameters', - 'canvasWebgl.pixels', - 'canvasWebgl.pixels2', - 'capturedErrors.data', - 'clientRects.domrectSystemSum', - 'clientRects.elementBoundingClientRect', - 'clientRects.elementClientRects', - 'clientRects.emojiSet', - 'clientRects.rangeBoundingClientRect', - 'clientRects.rangeClientRects', - 'consoleErrors.errors', - 'css.computedStyle', - 'css.system', - 'cssMedia.matchMediaCSS', - 'cssMedia.mediaCSS', - 'cssMedia.screenQuery', - 'features.cssFeatures', - 'features.cssVersion', - 'features.jsFeatures', - 'features.jsFeaturesKeys', - 'features.jsVersion', - 'features.version', - 'features.versionRange', - 'features.windowFeatures', - 'features.windowVersion', - 'fonts.apps', - 'fonts.emojiSet', - 'fonts.fontFaceLoadFonts', - 'fonts.pixelSizeSystemSum', - 'fonts.platformVersion', - 'headless.chromium', - 'headless.headless', - 'headless.headlessRating', - 'headless.likeHeadless', - 'headless.likeHeadlessRating', - 'headless.platformEstimate', - 'headless.stealth', - 'headless.stealthRating', - 'headless.systemFonts', - 'htmlElementVersion.keys', - 'intl.dateTimeFormat', - 'intl.displayNames', - 'intl.listFormat', - 'intl.locale', - 'intl.numberFormat', - 'intl.pluralRules', - 'intl.relativeTimeFormat', - 'lies.data', - 'lies.totalLies', - 'maths.data', - 'media.mimeTypes', - 'navigator.appVersion', - 'navigator.bluetoothAvailability', - 'navigator.device', - 'navigator.deviceMemory', - 'navigator.doNotTrack', - 'navigator.globalPrivacyControl', - 'navigator.hardwareConcurrency', - 'navigator.language', - 'navigator.maxTouchPoints', - 'navigator.mimeTypes', - 'navigator.oscpu', - 'navigator.permissions', - 'navigator.platform', - 'navigator.plugins', - 'navigator.properties', - 'navigator.system', - 'navigator.uaPostReduction', - 'navigator.userAgent', - 'navigator.userAgentData', - 'navigator.userAgentParsed', - 'navigator.vendor', - 'navigator.webgpu', - 'offlineAudioContext.binsSample', - 'offlineAudioContext.compressorGainReduction', - 'offlineAudioContext.copySample', - 'offlineAudioContext.floatFrequencyDataSum', - 'offlineAudioContext.floatTimeDomainDataSum', - 'offlineAudioContext.noise', - 'offlineAudioContext.sampleSum', - 'offlineAudioContext.totalUniqueSamples', - 'offlineAudioContext.values', - 'resistance.engine', - 'resistance.extension', - 'resistance.extensionHashPattern', - 'resistance.mode', - 'resistance.privacy', - 'resistance.security', - 'screen.availHeight', - 'screen.availWidth', - 'screen.colorDepth', - 'screen.height', - 'screen.pixelDepth', - 'screen.touch', - 'screen.width', - 'svg.bBox', - 'svg.computedTextLength', - 'svg.emojiSet', - 'svg.extentOfChar', - 'svg.subStringLength', - 'svg.svgrectSystemSum', - 'timezone.location', - 'timezone.locationEpoch', - 'timezone.locationMeasured', - 'timezone.offset', - 'timezone.offsetComputed', - 'timezone.zone', - 'trash.trashBin', - 'voices.defaultVoiceLang', - 'voices.defaultVoiceName', - 'voices.languages', - 'voices.local', - 'voices.remote', - 'windowFeatures.apple', - 'windowFeatures.keys', - 'windowFeatures.moz', - 'windowFeatures.webkit', - 'workerScope.device', - 'workerScope.deviceMemory', - 'workerScope.engineCurrencyLocale', - 'workerScope.gpu', - 'workerScope.hardwareConcurrency', - 'workerScope.language', - 'workerScope.languages', - 'workerScope.lies', - 'workerScope.locale', - 'workerScope.localeEntropyIsTrusty', - 'workerScope.localeIntlEntropyIsTrusty', - 'workerScope.platform', - 'workerScope.system', - 'workerScope.systemCurrencyLocale', - 'workerScope.timezoneLocation', - 'workerScope.timezoneOffset', - 'workerScope.uaPostReduction', - 'workerScope.userAgent', - 'workerScope.userAgentData', - 'workerScope.userAgentDataVersion', - 'workerScope.userAgentEngine', - 'workerScope.userAgentVersion', - 'workerScope.webglRenderer', - 'workerScope.webglVendor', - ]; - // construct map of all metrics - const metricsAll = Object.keys(fp).sort().reduce((acc, sectionKey) => { - const section = fp[sectionKey]; - const sectionMetrics = Object.keys(section || {}).sort().reduce((acc, key) => { - if (key == '$hash' || key == 'lied') { - return acc; - } - return { ...acc, [`${sectionKey}.${key}`]: section[key] }; - }, {}); - return { ...acc, ...sectionMetrics }; - }, {}); - // reduce to 64 bins - const maxBins = 64; - const metricKeysReported = Object.keys(metricsAll); - const binSize = Math.ceil(metricKeys.length / maxBins); - // update log - const devMode = window.location.host != 'abrahamjuliot.github.io'; - if (devMode && ('' + metricKeysReported != '' + metricKeys)) { - const newKeys = metricKeysReported.filter((key) => !metricKeys.includes(key)); - const oldKeys = metricKeys.filter((key) => !metricKeysReported.includes(key)); - if (newKeys.length || oldKeys.length) { - newKeys.length && console.warn('added fuzzy key(s):\n', newKeys.join('\n')); - oldKeys.length && console.warn('removed fuzzy key(s):\n', oldKeys.join('\n')); - console.groupCollapsed('update keys for accurate fuzzy hashing:'); - console.log(metricKeysReported.map((x) => `'${x}',`).join('\n')); - console.groupEnd(); - } - } - // compute fuzzy fingerprint master - const fuzzyFpMaster = metricKeys.reduce((acc, key, index) => { - if (!index || ((index % binSize) == 0)) { - const keySet = metricKeys.slice(index, index + binSize); - return { ...acc, ['' + keySet]: keySet.map((key) => metricsAll[key]) }; - } - return acc; - }, {}); - // hash each bin - await Promise.all(Object.keys(fuzzyFpMaster).map((key) => hashify(fuzzyFpMaster[key]).then((hash) => { - fuzzyFpMaster[key] = hash; // swap values for hash - return hash; - }))); - // create fuzzy hash - const fuzzyBits = 64; - const fuzzyFingerprint = Object.keys(fuzzyFpMaster) - .map((key) => fuzzyFpMaster[key][0]) - .join('') - .padEnd(fuzzyBits, '0'); - return fuzzyFingerprint; - }; - - const KnownAudio = { - // Blink/WebKit - [-20.538286209106445]: [ - 124.0434488439787, - 124.04344968475198, - 124.04347527516074, - 124.04347503720783, - 124.04347657808103, - ], - [-20.538288116455078]: [ - 124.04347518575378, - 124.04347527516074, - 124.04344884395687, - 124.04344968475198, - 124.04347657808103, - 124.04347730590962, // pattern (rare) - 124.0434765110258, // pattern (rare) - 124.04347656317987, // pattern (rare) - 124.04375314689969, // pattern (rare) - // WebKit - 124.0434485301812, - 124.0434496849557, - 124.043453265891, - 124.04345734833623, - 124.04345808873768, - ], - [-20.535268783569336]: [ - // Android/Linux - 124.080722568091, - 124.08072256811283, - 124.08072766105033, - 124.08072787802666, - 124.08072787804849, - 124.08074500028306, - 124.0807470110085, - 124.08075528279005, - 124.08075643483608, - ], - // Gecko - [-31.502187728881836]: [35.74996626004577], - [-31.502185821533203]: [35.74996031448245, 35.7499681673944, 35.749968223273754], - [-31.50218963623047]: [35.74996031448245], - [-31.509262084960938]: [35.7383295930922, 35.73833402246237], - // WebKit - [-29.837873458862305]: [35.10892717540264, 35.10892752557993], - [-29.83786964416504]: [35.10893232002854, 35.10893253237009], - }; - const AUDIO_TRAP = Math.random(); - async function hasFakeAudio() { - const context = new OfflineAudioContext(1, 100, 44100); - const oscillator = context.createOscillator(); - oscillator.frequency.value = 0; - oscillator.start(0); - context.startRendering(); - return new Promise((resolve) => { - context.oncomplete = (event) => { - const channelData = event.renderedBuffer.getChannelData?.(0); - if (!channelData) - resolve(false); - resolve('' + [...new Set(channelData)] !== '0'); - }; - }).finally(() => oscillator.disconnect()); - } - async function getOfflineAudioContext() { - try { - const timer = createTimer(); - await queueEvent(timer); - try { - // @ts-expect-error if unsupported - window.OfflineAudioContext = OfflineAudioContext || webkitOfflineAudioContext; - } - catch (err) { } - if (!window.OfflineAudioContext) { - logTestResult({ test: 'audio', passed: false }); - return; - } - // detect lies - const channelDataLie = lieProps['AudioBuffer.getChannelData']; - const copyFromChannelLie = lieProps['AudioBuffer.copyFromChannel']; - let lied = (channelDataLie || copyFromChannelLie) || false; - const bufferLen = 5000; - const context = new OfflineAudioContext(1, bufferLen, 44100); - const analyser = context.createAnalyser(); - const oscillator = context.createOscillator(); - const dynamicsCompressor = context.createDynamicsCompressor(); - const biquadFilter = context.createBiquadFilter(); - // detect lie - const dataArray = new Float32Array(analyser.frequencyBinCount); - analyser.getFloatFrequencyData?.(dataArray); - const floatFrequencyUniqueDataSize = new Set(dataArray).size; - if (floatFrequencyUniqueDataSize > 1) { - lied = true; - const floatFrequencyDataLie = `expected -Infinity (silence) and got ${floatFrequencyUniqueDataSize} frequencies`; - documentLie(`AnalyserNode.getFloatFrequencyData`, floatFrequencyDataLie); - } - const values = { - ['AnalyserNode.channelCount']: attempt(() => analyser.channelCount), - ['AnalyserNode.channelCountMode']: attempt(() => analyser.channelCountMode), - ['AnalyserNode.channelInterpretation']: attempt(() => analyser.channelInterpretation), - ['AnalyserNode.context.sampleRate']: attempt(() => analyser.context.sampleRate), - ['AnalyserNode.fftSize']: attempt(() => analyser.fftSize), - ['AnalyserNode.frequencyBinCount']: attempt(() => analyser.frequencyBinCount), - ['AnalyserNode.maxDecibels']: attempt(() => analyser.maxDecibels), - ['AnalyserNode.minDecibels']: attempt(() => analyser.minDecibels), - ['AnalyserNode.numberOfInputs']: attempt(() => analyser.numberOfInputs), - ['AnalyserNode.numberOfOutputs']: attempt(() => analyser.numberOfOutputs), - ['AnalyserNode.smoothingTimeConstant']: attempt(() => analyser.smoothingTimeConstant), - ['AnalyserNode.context.listener.forwardX.maxValue']: attempt(() => { - return caniuse(() => analyser.context.listener.forwardX.maxValue); - }), - ['BiquadFilterNode.gain.maxValue']: attempt(() => biquadFilter.gain.maxValue), - ['BiquadFilterNode.frequency.defaultValue']: attempt(() => biquadFilter.frequency.defaultValue), - ['BiquadFilterNode.frequency.maxValue']: attempt(() => biquadFilter.frequency.maxValue), - ['DynamicsCompressorNode.attack.defaultValue']: attempt(() => dynamicsCompressor.attack.defaultValue), - ['DynamicsCompressorNode.knee.defaultValue']: attempt(() => dynamicsCompressor.knee.defaultValue), - ['DynamicsCompressorNode.knee.maxValue']: attempt(() => dynamicsCompressor.knee.maxValue), - ['DynamicsCompressorNode.ratio.defaultValue']: attempt(() => dynamicsCompressor.ratio.defaultValue), - ['DynamicsCompressorNode.ratio.maxValue']: attempt(() => dynamicsCompressor.ratio.maxValue), - ['DynamicsCompressorNode.release.defaultValue']: attempt(() => dynamicsCompressor.release.defaultValue), - ['DynamicsCompressorNode.release.maxValue']: attempt(() => dynamicsCompressor.release.maxValue), - ['DynamicsCompressorNode.threshold.defaultValue']: attempt(() => dynamicsCompressor.threshold.defaultValue), - ['DynamicsCompressorNode.threshold.minValue']: attempt(() => dynamicsCompressor.threshold.minValue), - ['OscillatorNode.detune.maxValue']: attempt(() => oscillator.detune.maxValue), - ['OscillatorNode.detune.minValue']: attempt(() => oscillator.detune.minValue), - ['OscillatorNode.frequency.defaultValue']: attempt(() => oscillator.frequency.defaultValue), - ['OscillatorNode.frequency.maxValue']: attempt(() => oscillator.frequency.maxValue), - ['OscillatorNode.frequency.minValue']: attempt(() => oscillator.frequency.minValue), - }; - const getRenderedBuffer = (context) => (new Promise((resolve) => { - const analyser = context.createAnalyser(); - const oscillator = context.createOscillator(); - const dynamicsCompressor = context.createDynamicsCompressor(); - try { - oscillator.type = 'triangle'; - oscillator.frequency.value = 10000; - dynamicsCompressor.threshold.value = -50; - dynamicsCompressor.knee.value = 40; - dynamicsCompressor.attack.value = 0; - } - catch (err) { } - oscillator.connect(dynamicsCompressor); - dynamicsCompressor.connect(analyser); - dynamicsCompressor.connect(context.destination); - oscillator.start(0); - context.startRendering(); - return context.addEventListener('complete', (event) => { - try { - dynamicsCompressor.disconnect(); - oscillator.disconnect(); - const floatFrequencyData = new Float32Array(analyser.frequencyBinCount); - analyser.getFloatFrequencyData?.(floatFrequencyData); - const floatTimeDomainData = new Float32Array(analyser.fftSize); - if ('getFloatTimeDomainData' in analyser) { - analyser.getFloatTimeDomainData(floatTimeDomainData); - } - return resolve({ - floatFrequencyData, - floatTimeDomainData, - buffer: event.renderedBuffer, - compressorGainReduction: ( - // @ts-expect-error if unsupported - dynamicsCompressor.reduction.value || // webkit - dynamicsCompressor.reduction), - }); - } - catch (error) { - return resolve(null); - } - }); - })); - await queueEvent(timer); - const [audioData, audioIsFake,] = await Promise.all([ - getRenderedBuffer(new OfflineAudioContext(1, bufferLen, 44100)), - hasFakeAudio().catch(() => false), - ]); - const { floatFrequencyData, floatTimeDomainData, buffer, compressorGainReduction, } = audioData || {}; - await queueEvent(timer); - const getSnapshot = (arr, start, end) => { - const collection = []; - for (let i = start; i < end; i++) { - collection.push(arr[i]); - } - return collection; - }; - const getSum = (arr) => !arr ? 0 : [...arr] - .reduce((acc, curr) => (acc += Math.abs(curr)), 0); - const floatFrequencyDataSum = getSum(floatFrequencyData); - const floatTimeDomainDataSum = getSum(floatTimeDomainData); - const copy = new Float32Array(bufferLen); - let bins = new Float32Array(); - if (buffer) { - buffer.copyFromChannel?.(copy, 0); - bins = buffer.getChannelData?.(0) || []; - } - const copySample = getSnapshot([...copy], 4500, 4600); - const binsSample = getSnapshot([...bins], 4500, 4600); - const sampleSum = getSum(getSnapshot([...bins], 4500, bufferLen)); - // detect lies - if (audioIsFake) { - lied = true; - documentLie('AudioBuffer', 'audio is fake'); - } - // sample matching - const matching = '' + binsSample == '' + copySample; - const copyFromChannelSupported = ('copyFromChannel' in AudioBuffer.prototype); - if (copyFromChannelSupported && !matching) { - lied = true; - const audioSampleLie = 'getChannelData and copyFromChannel samples mismatch'; - documentLie('AudioBuffer', audioSampleLie); - } - // sample uniqueness - const totalUniqueSamples = new Set([...bins]).size; - if (totalUniqueSamples == bufferLen) { - const audioUniquenessTrash = `${totalUniqueSamples} unique samples of ${bufferLen} is too high`; - sendToTrash('AudioBuffer', audioUniquenessTrash); - } - // sample noise factor - const getRandFromRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; - const getCopyFrom = (rand, buffer, copy) => { - const { length } = buffer; - const max = 20; - const start = getRandFromRange(275, length - (max + 1)); - const mid = start + max / 2; - const end = start + max; - buffer.getChannelData(0)[start] = rand; - buffer.getChannelData(0)[mid] = rand; - buffer.getChannelData(0)[end] = rand; - buffer.copyFromChannel(copy, 0); - const attack = [ - buffer.getChannelData(0)[start] === 0 ? Math.random() : 0, - buffer.getChannelData(0)[mid] === 0 ? Math.random() : 0, - buffer.getChannelData(0)[end] === 0 ? Math.random() : 0, - ]; - return [...new Set([...buffer.getChannelData(0), ...copy, ...attack])].filter((x) => x !== 0); - }; - const getCopyTo = (rand, buffer, copy) => { - buffer.copyToChannel(copy.map(() => rand), 0); - const frequency = buffer.getChannelData(0)[0]; - const dataAttacked = [...buffer.getChannelData(0)] - .map((x) => x !== frequency || !x ? Math.random() : x); - return dataAttacked.filter((x) => x !== frequency); - }; - const getNoiseFactor = () => { - const length = 2000; - try { - const result = [...new Set([ - ...getCopyFrom(AUDIO_TRAP, new AudioBuffer({ length, sampleRate: 44100 }), new Float32Array(length)), - ...getCopyTo(AUDIO_TRAP, new AudioBuffer({ length, sampleRate: 44100 }), new Float32Array(length)), - ])]; - return +(result.length !== 1 && - result.reduce((acc, n) => acc += +n, 0)); - } - catch (error) { - console.error(error); - return 0; - } - }; - const noiseFactor = getNoiseFactor(); - const noise = (noiseFactor || [...new Set(bins.slice(0, 100))] - .reduce((acc, n) => acc += n, 0)); - // Locked Patterns - const known = { - /* BLINK */ - // 124.04347527516074/124.04347518575378 - '-20.538286209106445,164537.64796829224,502.5999283068122': [124.04347527516074], - '-20.538288116455078,164537.64796829224,502.5999283068122': [124.04347527516074], - '-20.538288116455078,164537.64795303345,502.5999283068122': [ - 124.04347527516074, - 124.04347518575378, - // sus: - 124.04347519320436, - 124.04347523045726, - ], - '-20.538286209106445,164537.64805984497,502.5999283068122': [124.04347527516074], - '-20.538288116455078,164537.64805984497,502.5999283068122': [ - 124.04347527516074, - 124.04347518575378, - // sus - 124.04347520065494, - 124.04347523790784, - 124.043475252809, - 124.04347526025958, - 124.04347522300668, - 124.04347523045726, - 124.04347524535842, - ], - // 124.04344884395687 - '-20.538288116455078,164881.9727935791,502.59990317908887': [124.04344884395687], - '-20.538288116455078,164881.9729309082,502.59990317908887': [124.04344884395687], - // 124.0434488439787 - '-20.538286209106445,164882.2082748413,502.59990317911434': [124.0434488439787], - '-20.538288116455078,164882.20836639404,502.59990317911434': [124.0434488439787], - // 124.04344968475198 - '-20.538286209106445,164863.45319366455,502.5999033495791': [124.04344968475198], - '-20.538288116455078,164863.45319366455,502.5999033495791': [ - 124.04344968475198, - 124.04375314689969, // rare - // sus - 124.04341541208123, - ], - // 124.04347503720783 (rare) - '-20.538288116455078,164531.82670593262,502.59992767886797': [ - 124.04347503720783, - // sus - 124.04347494780086, - 124.04347495525144, - 124.04347499250434, - 124.0434750074055, - ], - // 124.04347657808103 - '-20.538286209106445,164540.1567993164,502.59992209258417': [124.04347657808103], - '-20.538288116455078,164540.1567993164,502.59992209258417': [ - 124.04347657808103, - 124.0434765110258, // rare - 124.04347656317987, // rare - // sus - 124.04347657063045, - 124.04378004022874, - ], - '-20.538288116455078,164540.1580810547,502.59992209258417': [124.04347657808103], - // 124.080722568091/124.04347730590962 (rare) - '-20.535268783569336,164940.360786438,502.69695458233764': [124.080722568091], - '-20.538288116455078,164538.55073928833,502.5999307175407': [124.04347730590962], - // Android/Linux - '-20.535268783569336,164948.14596557617,502.6969545823631': [124.08072256811283], - '-20.535268783569336,164926.65912628174,502.6969610930064': [124.08072766105033], - '-20.535268783569336,164932.96168518066,502.69696179985476': [124.08072787802666], - '-20.535268783569336,164931.54252624512,502.6969617998802': [124.08072787804849], - '-20.535268783569336,164591.9659729004,502.6969925059784': [124.08074500028306], - '-20.535268783569336,164590.4111480713,502.6969947774742': [124.0807470110085], - '-20.535268783569336,164590.41115570068,502.6969947774742': [124.0807470110085], - '-20.535268783569336,164593.64263916016,502.69700490119067': [124.08075528279005], - '-20.535268783569336,164595.0285797119,502.69700578315314': [124.08075643483608], - // sus - '-20.538288116455078,164860.96576690674,502.6075748118915': [124.0434496279413], - '-20.538288116455078,164860.9938583374,502.6073723861407': [124.04344962817413], - '-20.538288116455078,164862.14078521729,502.59991004130643': [124.04345734833623], - '-20.538288116455078,164534.50047683716,502.61542110471055': [124.04347520368174], - '-20.538288116455078,164535.1324043274,502.6079200572931': [124.04347521997988], - '-20.538288116455078,164535.51135635376,502.60633126448374': [124.04347522952594], - /* GECKO */ - '-31.509262084960938,167722.6894454956,148.42717787250876': [35.7383295930922], - '-31.509262084960938,167728.72756958008,148.427184343338': [35.73833402246237], - '-31.50218963623047,167721.27517700195,148.47537828609347': [35.74996031448245], - '-31.502185821533203,167727.52931976318,148.47542023658752': [35.7499681673944], - '-31.502185821533203,167700.7530517578,148.475412953645': [35.749968223273754], - '-31.502187728881836,167697.23177337646,148.47541113197803': [35.74996626004577], - /* WEBKIT */ - '-20.538288116455078,164873.80361557007,502.59989904452596': [124.0434485301812], - '-20.538288116455078,164863.47760391235,502.5999033453372': [124.0434496849557], - '-20.538288116455078,164876.62466049194,502.5998911961724': [124.043453265891], - '-20.538288116455078,164862.14879989624,502.59991004130643': [124.04345734833623], - '-20.538288116455078,164896.54167175293,502.5999054916465': [124.04345808873768], - '-29.837873458862305,163206.43050384521,0': [35.10892717540264], - '-29.837873458862305,163224.69785308838,0': [35.10892752557993], - '-29.83786964416504,163209.17245483398,0': [35.10893232002854], - '-29.83786964416504,163202.77336883545,0': [35.10893253237009], - }; - if (noise) { - lied = true; - documentLie('AudioBuffer', 'sample noise detected'); - } - const pattern = '' + [ - compressorGainReduction, - floatFrequencyDataSum, - floatTimeDomainDataSum, - ]; - const knownPattern = known[pattern]; - if (knownPattern && !knownPattern.includes(sampleSum)) { - LowerEntropy.AUDIO = true; - sendToTrash('AudioBuffer', 'suspicious frequency data'); - } - logTestResult({ time: timer.stop(), test: 'audio', passed: true }); - return { - totalUniqueSamples, - compressorGainReduction, - floatFrequencyDataSum, - floatTimeDomainDataSum, - sampleSum, - binsSample, - copySample: copyFromChannelSupported ? copySample : [undefined], - values, - noise, - lied, - }; - } - catch (error) { - logTestResult({ test: 'audio', passed: false }); - captureError(error, 'OfflineAudioContext failed or blocked by client'); - return; - } - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function audioHTML(fp) { - if (!fp.offlineAudioContext) { - return `
- Audio -
sum: ${HTMLNote.BLOCKED}
-
gain: ${HTMLNote.BLOCKED}
-
freq: ${HTMLNote.BLOCKED}
-
time: ${HTMLNote.BLOCKED}
-
trap: ${HTMLNote.BLOCKED}
-
unique: ${HTMLNote.BLOCKED}
-
data: ${HTMLNote.BLOCKED}
-
copy: ${HTMLNote.BLOCKED}
-
values: ${HTMLNote.BLOCKED}
-
`; - } - const { offlineAudioContext: { $hash, totalUniqueSamples, compressorGainReduction, floatFrequencyDataSum, floatTimeDomainDataSum, sampleSum, binsSample, copySample, lied, noise, values, }, } = fp; - const knownSums = KnownAudio[compressorGainReduction] || []; - const validAudio = sampleSum && compressorGainReduction && knownSums.length; - const matchesKnownAudio = knownSums.includes(sampleSum); - return ` -
- ${performanceLogger.getLog().audio} - Audio${hashSlice($hash)} -
sum: ${!sampleSum ? HTMLNote.BLOCKED : (!validAudio || matchesKnownAudio) ? sampleSum : getDiffs({ - stringA: knownSums[0], - stringB: sampleSum, - charDiff: true, - decorate: (diff) => `${diff}`, - })}
-
gain: ${compressorGainReduction || HTMLNote.BLOCKED}
-
freq: ${floatFrequencyDataSum || HTMLNote.BLOCKED}
-
time: ${floatTimeDomainDataSum || HTMLNote.UNSUPPORTED}
-
trap: ${!noise ? AUDIO_TRAP : getDiffs({ - stringA: AUDIO_TRAP, - stringB: noise, - charDiff: true, - decorate: (diff) => `${diff}`, - })}
-
unique: ${totalUniqueSamples}
-
data:${'' + binsSample[0] == 'undefined' ? ` ${HTMLNote.BLOCKED}` : - `${hashMini(binsSample)}`}
-
copy:${'' + copySample[0] == 'undefined' ? ` ${HTMLNote.BLOCKED}` : - `${hashMini(copySample)}`}
-
values: ${modal('creep-offline-audio-context', Object.keys(values).map((key) => `
${key}: ${values[key]}
`).join(''), hashMini(values))}
-
- `; - } - - // inspired by https://arkenfox.github.io/TZP/tests/canvasnoise.html - let pixelImageRandom = ''; - const getPixelMods = () => { - const pattern1 = []; - const pattern2 = []; - const len = 8; // canvas dimensions - const alpha = 255; - const visualMultiplier = 5; - try { - // create 2 canvas contexts - const options = { - willReadFrequently: true, - desynchronized: true, - }; - const canvasDisplay1 = document.createElement('canvas'); - const canvasDisplay2 = document.createElement('canvas'); - const canvas1 = document.createElement('canvas'); - const canvas2 = document.createElement('canvas'); - const contextDisplay1 = canvasDisplay1.getContext('2d', options); - const contextDisplay2 = canvasDisplay2.getContext('2d', options); - const context1 = canvas1.getContext('2d', options); - const context2 = canvas2.getContext('2d', options); - if (!contextDisplay1 || !contextDisplay2 || !context1 || !context2) { - throw new Error('canvas context blocked'); - } - // set the dimensions - canvasDisplay1.width = len * visualMultiplier; - canvasDisplay1.height = len * visualMultiplier; - canvasDisplay2.width = len * visualMultiplier; - canvasDisplay2.height = len * visualMultiplier; - canvas1.width = len; - canvas1.height = len; - canvas2.width = len; - canvas2.height = len; - [...Array(len)].forEach((e, x) => [...Array(len)].forEach((e, y) => { - const red = ~~(Math.random() * 256); - const green = ~~(Math.random() * 256); - const blue = ~~(Math.random() * 256); - const colors = `${red}, ${green}, ${blue}, ${alpha}`; - context1.fillStyle = `rgba(${colors})`; - context1.fillRect(x, y, 1, 1); - // capture data in visuals - contextDisplay1.fillStyle = `rgba(${colors})`; - contextDisplay1.fillRect(x * visualMultiplier, y * visualMultiplier, 1 * visualMultiplier, 1 * visualMultiplier); - return pattern1.push(colors); // collect the pixel pattern - })); - [...Array(len)].forEach((e, x) => [...Array(len)].forEach((e, y) => { - // get context1 pixel data and mirror to context2 - const { data: [red, green, blue, alpha], } = context1.getImageData(x, y, 1, 1) || {}; - const colors = `${red}, ${green}, ${blue}, ${alpha}`; - context2.fillStyle = `rgba(${colors})`; - context2.fillRect(x, y, 1, 1); - // capture noise in visuals - const { data: [red2, green2, blue2, alpha2], } = context2.getImageData(x, y, 1, 1) || {}; - const colorsDisplay = ` - ${red != red2 ? red2 : 255}, - ${green != green2 ? green2 : 255}, - ${blue != blue2 ? blue2 : 255}, - ${alpha != alpha2 ? alpha2 : 1} - `; - contextDisplay2.fillStyle = `rgba(${colorsDisplay})`; - contextDisplay2.fillRect(x * visualMultiplier, y * visualMultiplier, 1 * visualMultiplier, 1 * visualMultiplier); - return pattern2.push(colors); // collect the pixel pattern - })); - // compare the pattern collections and collect diffs - const patternDiffs = []; - const rgbaChannels = new Set(); - [...Array(pattern1.length)].forEach((e, i) => { - const pixelColor1 = pattern1[i]; - const pixelColor2 = pattern2[i]; - if (pixelColor1 != pixelColor2) { - const rgbaValues1 = pixelColor1.split(','); - const rgbaValues2 = pixelColor2.split(','); - const colors = [ - rgbaValues1[0] != rgbaValues2[0] ? 'r' : '', - rgbaValues1[1] != rgbaValues2[1] ? 'g' : '', - rgbaValues1[2] != rgbaValues2[2] ? 'b' : '', - rgbaValues1[3] != rgbaValues2[3] ? 'a' : '', - ].join(''); - rgbaChannels.add(colors); - patternDiffs.push([i, colors]); - } - }); - pixelImageRandom = canvasDisplay1.toDataURL(); // template use only - const pixelImage = canvasDisplay2.toDataURL(); - const rgba = rgbaChannels.size ? [...rgbaChannels].sort().join(', ') : undefined; - const pixels = patternDiffs.length || undefined; - return { rgba, pixels, pixelImage }; - } - catch (error) { - return console.error(error); - } - }; - // based on and inspired by https://github.com/antoinevastel/picasso-like-canvas-fingerprinting - const paintCanvas = ({ canvas, context, strokeText = false, cssFontFamily = '', area = { width: 50, height: 50 }, rounds = 10, maxShadowBlur = 50, seed = 500, offset = 2001000001, multiplier = 15000, }) => { - if (!context) { - return; - } - context.clearRect(0, 0, canvas.width, canvas.height); - canvas.width = area.width; - canvas.height = area.height; - if (canvas.style) { - canvas.style.display = 'none'; - } - const createPicassoSeed = ({ seed, offset, multiplier }) => { - let current = Number(seed) % Number(offset); - const getNextSeed = () => { - current = (Number(multiplier) * current) % Number(offset); - return current; - }; - return { - getNextSeed, - }; - }; - const picassoSeed = createPicassoSeed({ seed, offset, multiplier }); - const { getNextSeed } = picassoSeed; - const patchSeed = (current, offset, maxBound, computeFloat) => { - const result = (((current - 1) / offset) * (maxBound || 1)) || 0; - return computeFloat ? result : Math.floor(result); - }; - const addRandomCanvasGradient = (context, offset, area, colors, getNextSeed) => { - const { width, height } = area; - const canvasGradient = context.createRadialGradient(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width)); - canvasGradient.addColorStop(0, colors[patchSeed(getNextSeed(), offset, colors.length)]); - canvasGradient.addColorStop(1, colors[patchSeed(getNextSeed(), offset, colors.length)]); - context.fillStyle = canvasGradient; - }; - const colors = [ - '#FF6633', '#FFB399', '#FF33FF', '#FFFF99', '#00B3E6', - '#E6B333', '#3366E6', '#999966', '#99FF99', '#B34D4D', - '#80B300', '#809900', '#E6B3B3', '#6680B3', '#66991A', - '#FF99E6', '#CCFF1A', '#FF1A66', '#E6331A', '#33FFCC', - '#66994D', '#B366CC', '#4D8000', '#B33300', '#CC80CC', - '#66664D', '#991AFF', '#E666FF', '#4DB3FF', '#1AB399', - '#E666B3', '#33991A', '#CC9999', '#B3B31A', '#00E680', - '#4D8066', '#809980', '#E6FF80', '#1AFF33', '#999933', - '#FF3380', '#CCCC00', '#66E64D', '#4D80CC', '#9900B3', - '#E64D66', '#4DB380', '#FF4D4D', '#99E6E6', '#6666FF', - ]; - const drawOutlineOfText = (context, offset, area, getNextSeed) => { - const { width, height } = area; - const fontSize = 2.99; - context.font = `${height / fontSize}px ${cssFontFamily.replace(/!important/gm, '')}`; - context.strokeText('👾A', patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width)); - }; - const createCircularArc = (context, offset, area, getNextSeed) => { - const { width, height } = area; - context.beginPath(); - context.arc(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, Math.min(width, height)), patchSeed(getNextSeed(), offset, 2 * Math.PI, true), patchSeed(getNextSeed(), offset, 2 * Math.PI, true)); - context.stroke(); - }; - const createBezierCurve = (context, offset, area, getNextSeed) => { - const { width, height } = area; - context.beginPath(); - context.moveTo(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height)); - context.bezierCurveTo(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height)); - context.stroke(); - }; - const createQuadraticCurve = (context, offset, area, getNextSeed) => { - const { width, height } = area; - context.beginPath(); - context.moveTo(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height)); - context.quadraticCurveTo(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height)); - context.stroke(); - }; - const createEllipticalArc = (context, offset, area, getNextSeed) => { - if (!('ellipse' in context)) { - return; - } - const { width, height } = area; - context.beginPath(); - context.ellipse(patchSeed(getNextSeed(), offset, width), patchSeed(getNextSeed(), offset, height), patchSeed(getNextSeed(), offset, Math.floor(width / 2)), patchSeed(getNextSeed(), offset, Math.floor(height / 2)), patchSeed(getNextSeed(), offset, 2 * Math.PI, true), patchSeed(getNextSeed(), offset, 2 * Math.PI, true), patchSeed(getNextSeed(), offset, 2 * Math.PI, true)); - context.stroke(); - }; - const methods = [ - createCircularArc, - createBezierCurve, - createQuadraticCurve, - ]; - if (!IS_WEBKIT) - methods.push(createEllipticalArc); // unstable in webkit - if (strokeText) - methods.push(drawOutlineOfText); - [...Array(rounds)].forEach((x) => { - addRandomCanvasGradient(context, offset, area, colors, getNextSeed); - context.shadowBlur = patchSeed(getNextSeed(), offset, maxShadowBlur, true); - context.shadowColor = colors[patchSeed(getNextSeed(), offset, colors.length)]; - const nextMethod = methods[patchSeed(getNextSeed(), offset, methods.length)]; - nextMethod(context, offset, area, getNextSeed); - context.fill(); - }); - return; - }; - async function getCanvas2d() { - try { - const timer = createTimer(); - await queueEvent(timer); - const dataLie = lieProps['HTMLCanvasElement.toDataURL']; - const contextLie = lieProps['HTMLCanvasElement.getContext']; - const imageDataLie = (lieProps['CanvasRenderingContext2D.fillText'] || - lieProps['CanvasRenderingContext2D.font'] || - lieProps['CanvasRenderingContext2D.getImageData'] || - lieProps['CanvasRenderingContext2D.strokeText']); - const codePointLie = lieProps['String.fromCodePoint']; - let textMetricsLie = (lieProps['CanvasRenderingContext2D.measureText'] || - lieProps['TextMetrics.actualBoundingBoxAscent'] || - lieProps['TextMetrics.actualBoundingBoxDescent'] || - lieProps['TextMetrics.actualBoundingBoxLeft'] || - lieProps['TextMetrics.actualBoundingBoxRight'] || - lieProps['TextMetrics.fontBoundingBoxAscent'] || - lieProps['TextMetrics.fontBoundingBoxDescent'] || - lieProps['TextMetrics.width']); - let lied = (dataLie || - contextLie || - imageDataLie || - textMetricsLie || - codePointLie) || false; - // create canvas context - let win = window; - if (!LIKE_BRAVE && PHANTOM_DARKNESS) { - win = PHANTOM_DARKNESS; - } - const doc = win.document; - const canvas = doc.createElement('canvas'); - const context = canvas.getContext('2d'); - const canvasCPU = doc.createElement('canvas'); - const contextCPU = canvasCPU.getContext('2d', { - desynchronized: true, - willReadFrequently: true, - }); - if (!context) { - throw new Error('canvas context blocked'); - } - await queueEvent(timer); - const imageSizeMax = IS_WEBKIT ? 50 : 75; // webkit is unstable - paintCanvas({ - canvas, - context, - strokeText: true, - cssFontFamily: CSS_FONT_FAMILY, - area: { width: imageSizeMax, height: imageSizeMax }, - rounds: 10, - }); - const dataURI = canvas.toDataURL(); - await queueEvent(timer); - const mods = getPixelMods(); - // TextMetrics: get emoji set and system - await queueEvent(timer); - context.font = `10px ${CSS_FONT_FAMILY.replace(/!important/gm, '')}`; - const pattern = new Set(); - const emojiSet = EMOJIS.reduce((emojiSet, emoji) => { - const { actualBoundingBoxAscent, actualBoundingBoxDescent, actualBoundingBoxLeft, actualBoundingBoxRight, fontBoundingBoxAscent, fontBoundingBoxDescent, width, } = context.measureText(emoji) || {}; - const dimensions = [ - actualBoundingBoxAscent, - actualBoundingBoxDescent, - actualBoundingBoxLeft, - actualBoundingBoxRight, - fontBoundingBoxAscent, - fontBoundingBoxDescent, - width, - ].join(','); - if (!pattern.has(dimensions)) { - pattern.add(dimensions); - emojiSet.add(emoji); - } - return emojiSet; - }, new Set()); - // textMetrics System Sum - const textMetricsSystemSum = 0.00001 * [...pattern].map((x) => { - return x.split(',').reduce((acc, x) => acc += (+x || 0), 0); - }).reduce((acc, x) => acc += x, 0); - // Paint - const maxSize = 75; - await queueEvent(timer); - paintCanvas({ - canvas, - context, - area: { width: maxSize, height: maxSize }, - }); // clears image - const paintURI = canvas.toDataURL(); - // Paint with CPU - await queueEvent(timer); - paintCanvas({ - canvas: canvasCPU, - context: contextCPU, - area: { width: maxSize, height: maxSize }, - }); // clears image - const paintCpuURI = canvasCPU.toDataURL(); - // Text - context.restore(); - context.clearRect(0, 0, canvas.width, canvas.height); - canvas.width = 50; - canvas.height = 50; - context.font = `50px ${CSS_FONT_FAMILY.replace(/!important/gm, '')}`; - context.fillText('A', 7, 37); - const textURI = canvas.toDataURL(); - // Emoji - context.restore(); - context.clearRect(0, 0, canvas.width, canvas.height); - canvas.width = 50; - canvas.height = 50; - context.font = `35px ${CSS_FONT_FAMILY.replace(/!important/gm, '')}`; - context.fillText('👾', 0, 37); - const emojiURI = canvas.toDataURL(); - // lies - context.clearRect(0, 0, canvas.width, canvas.height); - if ((mods && mods.pixels) || !!Math.max(...context.getImageData(0, 0, 8, 8).data)) { - lied = true; - documentLie(`CanvasRenderingContext2D.getImageData`, `pixel data modified`); - } - // verify low entropy image data - canvas.width = 2; - canvas.height = 2; - context.fillStyle = '#000'; - context.fillRect(0, 0, canvas.width, canvas.height); - context.fillStyle = '#fff'; - context.fillRect(2, 2, 1, 1); - context.beginPath(); - context.arc(0, 0, 2, 0, 1, true); - context.closePath(); - context.fill(); - const imageDataLowEntropy = context.getImageData(0, 0, 2, 2).data.join(''); - const KnownImageData = { - BLINK: [ - '255255255255178178178255246246246255555555255', - '255255255255192192192255240240240255484848255', - '255255255255177177177255246246246255535353255', - '255255255255128128128255191191191255646464255', - '255255255255178178178255247247247255565656255', // ? - '255255255255174174174255242242242255474747255', - '255255255255229229229255127127127255686868255', - '255255255255192192192255244244244255535353255', - ], - GECKO: [ - '255255255255191191191255207207207255646464255', - '255255255255192192192255240240240255484848255', - '255255255255191191191255239239239255646464255', - '255255255255191191191255223223223255606060255', // ? - '255255255255171171171255223223223255606060255', // ? - '255255255255188188188255245245245255525252255', - ], - WEBKIT: [ - '255255255255185185185255233233233255474747255', - '255255255255185185185255229229229255474747255', - '255255255255185185185255218218218255474747255', - '255255255255192192192255240240240255484848255', - '255255255255178178178255247247247255565656255', - '255255255255178178178255247247247255565656255', - '255255255255192192192255240240240255484848255', - '255255255255186186186255218218218255464646255', - ], - }; - Analysis.imageDataLowEntropy = imageDataLowEntropy; - if (IS_BLINK && !KnownImageData.BLINK.includes(imageDataLowEntropy)) { - LowerEntropy.CANVAS = true; - } - else if (IS_GECKO && !KnownImageData.GECKO.includes(imageDataLowEntropy)) { - LowerEntropy.CANVAS = true; - } - else if (IS_WEBKIT && !KnownImageData.WEBKIT.includes(imageDataLowEntropy)) { - LowerEntropy.CANVAS = true; - } - if (LowerEntropy.CANVAS) { - sendToTrash('CanvasRenderingContext2D.getImageData', 'suspicious pixel data'); - } - const getTextMetricsFloatLie = (context) => { - const isFloat = (n) => n % 1 !== 0; - const { actualBoundingBoxAscent: abba, actualBoundingBoxDescent: abbd, actualBoundingBoxLeft: abbl, actualBoundingBoxRight: abbr, fontBoundingBoxAscent: fbba, fontBoundingBoxDescent: fbbd, - // width: w, - } = context.measureText('') || {}; - const lied = [ - abba, - abbd, - abbl, - abbr, - fbba, - fbbd, - ].find((x) => isFloat((x || 0))); - return lied; - }; - await queueEvent(timer); - if (getTextMetricsFloatLie(context)) { - textMetricsLie = true; - lied = true; - documentLie('CanvasRenderingContext2D.measureText', 'metric noise detected'); - } - logTestResult({ time: timer.stop(), test: 'canvas 2d', passed: true }); - return { - dataURI, - paintURI, - paintCpuURI, - textURI, - emojiURI, - mods, - textMetricsSystemSum, - liedTextMetrics: textMetricsLie, - emojiSet: [...emojiSet], - lied, - }; - } - catch (error) { - logTestResult({ test: 'canvas 2d', passed: false }); - captureError(error); - return; - } - } - function canvasHTML(fp) { - if (!fp.canvas2d) { - return ` -
- Canvas 2d ${HTMLNote.BLOCKED} -
data: ${HTMLNote.BLOCKED}
-
rendering:
-
${HTMLNote.BLOCKED}
-
${HTMLNote.BLOCKED}
-
textMetrics:
-
${HTMLNote.BLOCKED}
-
`; - } - const { canvas2d: { lied, dataURI, paintURI, paintCpuURI, textURI, emojiURI, mods, emojiSet, textMetricsSystemSum, $hash, }, } = fp; - const { pixels, rgba, pixelImage } = mods || {}; - const modPercent = pixels ? Math.round((pixels / 400) * 100) : 0; - const hash = { - dataURI: hashMini(dataURI), - textURI: hashMini(textURI), - emojiURI: hashMini(emojiURI), - paintURI: hashMini(paintURI), - paintCpuURI: hashMini(paintCpuURI), - }; - const dataTemplate = ` - ${textURI ? `
` : ''} -
text: ${!textURI ? HTMLNote.BLOCKED : hash.textURI} - -

- ${emojiURI ? `
` : ''} -
emoji: ${!emojiURI ? HTMLNote.BLOCKED : hash.emojiURI} - -

- ${paintURI ? `
` : ''} -
paint (GPU): ${!paintURI ? HTMLNote.BLOCKED : hash.paintURI} - -

- ${paintCpuURI ? `
` : ''} -
paint (CPU): ${!paintCpuURI ? HTMLNote.BLOCKED : hash.paintCpuURI} - -

- ${dataURI ? `
` : ''} -
combined: ${!dataURI ? HTMLNote.BLOCKED : hash.dataURI} - `; - // rgba: "b, g, gb, r, rb, rg, rgb" - const rgbaHTML = !rgba ? rgba : rgba.split(', ').map((s) => s.split('').map((c) => { - const css = { - r: 'red', - g: 'green', - b: 'blue', - }; - return ``; - }).join('')).join(' '); - const emojiHelpTitle = `CanvasRenderingContext2D.measureText()\nhash: ${hashMini(emojiSet)}\n${emojiSet.map((x, i) => i && (i % 6 == 0) ? `${x}\n` : x).join('')}`; - return ` -
- - ${performanceLogger.getLog()['canvas 2d']} - Canvas 2d${hashSlice($hash)} -
data: ${modal('creep-canvas-data', dataTemplate, hashMini({ - dataURI, - pixelImage, - paintURI, - paintCpuURI, - textURI, - emojiURI, - }))}
-
rendering: ${rgba ? `${modPercent}% rgba noise ${rgbaHTML}` : ''}
-
- ${textURI ? `
` : ''} - ${emojiURI ? `
` : ''} - ${paintCpuURI ? `
` : ''} - ${dataURI ? `
` : ''} -
-
-
- ${rgba ? `
` : ''} -
-
textMetrics:
-
- ${textMetricsSystemSum || HTMLNote.UNSUPPORTED} - - ${formatEmojiSet(emojiSet)} - -
-
- `; - } - - function getCSS() { - const computeStyle = (type, { require: [captureError] }) => { - try { - // get CSSStyleDeclaration - const cssStyleDeclaration = (type == 'getComputedStyle' ? getComputedStyle(document.body) : - type == 'HTMLElement.style' ? document.body.style : - // @ts-ignore - type == 'CSSRuleList.style' ? document.styleSheets[0].cssRules[0].style : - undefined); - if (!cssStyleDeclaration) { - throw new TypeError('invalid argument string'); - } - // get properties - const proto = Object.getPrototypeOf(cssStyleDeclaration); - const prototypeProperties = Object.getOwnPropertyNames(proto); - const ownEnumerablePropertyNames = []; - const cssVar = /^--.*$/; - Object.keys(cssStyleDeclaration).forEach((key) => { - const numericKey = !isNaN(+key); - const value = cssStyleDeclaration[key]; - const customPropKey = cssVar.test(key); - const customPropValue = cssVar.test(value); - if (numericKey && !customPropValue) { - return ownEnumerablePropertyNames.push(value); - } - else if (!numericKey && !customPropKey) { - return ownEnumerablePropertyNames.push(key); - } - return; - }); - // get properties in prototype chain (required only in chrome) - const propertiesInPrototypeChain = {}; - const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1); - const uncapitalize = (str) => str.charAt(0).toLowerCase() + str.slice(1); - const removeFirstChar = (str) => str.slice(1); - const caps = /[A-Z]/g; - ownEnumerablePropertyNames.forEach((key) => { - if (propertiesInPrototypeChain[key]) { - return; - } - // determine attribute type - const isNamedAttribute = key.indexOf('-') > -1; - const isAliasAttribute = caps.test(key); - // reduce key for computation - const firstChar = key.charAt(0); - const isPrefixedName = isNamedAttribute && firstChar == '-'; - const isCapitalizedAlias = isAliasAttribute && firstChar == firstChar.toUpperCase(); - key = (isPrefixedName ? removeFirstChar(key) : - isCapitalizedAlias ? uncapitalize(key) : - key); - // find counterpart in CSSStyleDeclaration object or its prototype chain - if (isNamedAttribute) { - const aliasAttribute = key.split('-').map((word, index) => index == 0 ? word : capitalize(word)).join(''); - if (aliasAttribute in cssStyleDeclaration) { - propertiesInPrototypeChain[aliasAttribute] = true; - } - else if (capitalize(aliasAttribute) in cssStyleDeclaration) { - propertiesInPrototypeChain[capitalize(aliasAttribute)] = true; - } - } - else if (isAliasAttribute) { - const namedAttribute = key.replace(caps, (char) => '-' + char.toLowerCase()); - if (namedAttribute in cssStyleDeclaration) { - propertiesInPrototypeChain[namedAttribute] = true; - } - else if (`-${namedAttribute}` in cssStyleDeclaration) { - propertiesInPrototypeChain[`-${namedAttribute}`] = true; - } - } - return; - }); - // compile keys - const keys = [ - ...new Set([ - ...prototypeProperties, - ...ownEnumerablePropertyNames, - ...Object.keys(propertiesInPrototypeChain), - ]), - ]; - // @ts-ignore - const interfaceName = ('' + proto).match(/\[object (.+)\]/)[1]; - return { keys, interfaceName }; - } - catch (error) { - captureError(error); - return; - } - }; - const getSystemStyles = (el) => { - try { - const colors = [ - 'ActiveBorder', - 'ActiveCaption', - 'ActiveText', - 'AppWorkspace', - 'Background', - 'ButtonBorder', - 'ButtonFace', - 'ButtonHighlight', - 'ButtonShadow', - 'ButtonText', - 'Canvas', - 'CanvasText', - 'CaptionText', - 'Field', - 'FieldText', - 'GrayText', - 'Highlight', - 'HighlightText', - 'InactiveBorder', - 'InactiveCaption', - 'InactiveCaptionText', - 'InfoBackground', - 'InfoText', - 'LinkText', - 'Mark', - 'MarkText', - 'Menu', - 'MenuText', - 'Scrollbar', - 'ThreeDDarkShadow', - 'ThreeDFace', - 'ThreeDHighlight', - 'ThreeDLightShadow', - 'ThreeDShadow', - 'VisitedText', - 'Window', - 'WindowFrame', - 'WindowText', - ]; - const fonts = [ - 'caption', - 'icon', - 'menu', - 'message-box', - 'small-caption', - 'status-bar', - ]; - const getStyles = (el) => ({ - colors: colors.map((color) => { - el.setAttribute('style', `background-color: ${color} !important`); - return { - [color]: getComputedStyle(el).backgroundColor, - }; - }), - fonts: fonts.map((font) => { - el.setAttribute('style', `font: ${font} !important`); - const computedStyle = getComputedStyle(el); - return { - [font]: `${computedStyle.fontSize} ${computedStyle.fontFamily}`, - }; - }), - }); - if (!el) { - el = document.createElement('div'); - document.body.append(el); - const systemStyles = getStyles(el); - el.parentNode.removeChild(el); - return systemStyles; - } - return getStyles(el); - } - catch (error) { - captureError(error); - return; - } - }; - try { - const timer = createTimer(); - timer.start(); - const computedStyle = computeStyle('getComputedStyle', { require: [captureError] }); - const system = getSystemStyles(PARENT_PHANTOM); - logTestResult({ time: timer.stop(), test: 'computed style', passed: true }); - return { - computedStyle, - system, - }; - } - catch (error) { - logTestResult({ test: 'computed style', passed: false }); - captureError(error); - return; - } - } - function cssHTML(fp) { - if (!fp.css) { - return ` -
- Computed Style -
keys (0): ${HTMLNote.BLOCKED}
-
system styles: ${HTMLNote.BLOCKED}
-
-
`; - } - const { css: data, } = fp; - const { $hash, computedStyle, system, } = data; - const colorsLen = system.colors.length; - const gradientColors = system.colors.map((color, index) => { - const name = Object.values(color)[0]; - return (index == 0 ? `${name}, ${name} ${((index + 1) / colorsLen * 100).toFixed(2)}%` : - index == colorsLen - 1 ? `${name} ${((index - 1) / colorsLen * 100).toFixed(2)}%, ${name} 100%` : - `${name} ${(index / colorsLen * 100).toFixed(2)}%, ${name} ${((index + 1) / colorsLen * 100).toFixed(2)}%`); - }); - const id = 'creep-css-style-declaration-version'; - return ` -
- ${performanceLogger.getLog()['computed style']} - Computed Style${hashSlice($hash)} -
keys (${!computedStyle ? '0' : count(computedStyle.keys)}): ${!computedStyle ? HTMLNote.BLOCKED : - modal('creep-computed-style', computedStyle.keys.join(', '), hashMini(computedStyle))}
-
system styles: ${system && system.colors ? modal(`${id}-system-styles`, [ - ...system.colors.map((color) => { - const key = Object.keys(color)[0]; - const val = color[key]; - return ` -
${key}: ${val}
- `; - }), - ...system.fonts.map((font) => { - const key = Object.keys(font)[0]; - const val = font[key]; - return ` -
${key}: ${val}
- `; - }), - ].join(''), hashMini(system)) : HTMLNote.BLOCKED}
- -
-
- `; - } - - function getCSSMedia() { - const gcd = (a, b) => b == 0 ? a : gcd(b, a % b); - const getAspectRatio = (width, height) => { - const r = gcd(width, height); - const aspectRatio = `${width / r}/${height / r}`; - return aspectRatio; - }; - const query = ({ body, type, rangeStart, rangeLen }) => { - const html = [...Array(rangeLen)].map((slot, i) => { - i += rangeStart; - return `@media(device-${type}:${i}px){body{--device-${type}:${i};}}`; - }).join(''); - body.innerHTML = ``; - const style = getComputedStyle(body); - return style.getPropertyValue(`--device-${type}`).trim(); - }; - const getScreenMedia = ({ body, width, height }) => { - let widthMatch = query({ body, type: 'width', rangeStart: width, rangeLen: 1 }); - let heightMatch = query({ body, type: 'height', rangeStart: height, rangeLen: 1 }); - if (widthMatch && heightMatch) { - return { width, height }; - } - const rangeLen = 1000; - [...Array(10)].find((slot, i) => { - if (!widthMatch) { - widthMatch = query({ body, type: 'width', rangeStart: i * rangeLen, rangeLen }); - } - if (!heightMatch) { - heightMatch = query({ body, type: 'height', rangeStart: i * rangeLen, rangeLen }); - } - return widthMatch && heightMatch; - }); - return { width: +widthMatch, height: +heightMatch }; - }; - try { - const timer = createTimer(); - timer.start(); - const win = PHANTOM_DARKNESS.window; - const { body } = win.document; - const { width, availWidth, height, availHeight } = win.screen; - const noTaskbar = !(width - availWidth || height - availHeight); - if (screen.width !== width || (width > 800 && noTaskbar)) { - LowerEntropy.IFRAME_SCREEN = true; - } - const deviceAspectRatio = getAspectRatio(width, height); - const matchMediaCSS = { - ['prefers-reduced-motion']: (win.matchMedia('(prefers-reduced-motion: no-preference)').matches ? 'no-preference' : - win.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'reduce' : undefined), - ['prefers-color-scheme']: ( - // prefer main window - matchMedia('(prefers-color-scheme: light)').matches ? 'light' : - matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : undefined), - monochrome: (win.matchMedia('(monochrome)').matches ? 'monochrome' : - win.matchMedia('(monochrome: 0)').matches ? 'non-monochrome' : undefined), - ['inverted-colors']: (win.matchMedia('(inverted-colors: inverted)').matches ? 'inverted' : - win.matchMedia('(inverted-colors: none)').matches ? 'none' : undefined), - ['forced-colors']: (win.matchMedia('(forced-colors: none)').matches ? 'none' : - win.matchMedia('(forced-colors: active)').matches ? 'active' : undefined), - ['any-hover']: (win.matchMedia('(any-hover: hover)').matches ? 'hover' : - win.matchMedia('(any-hover: none)').matches ? 'none' : undefined), - hover: (win.matchMedia('(hover: hover)').matches ? 'hover' : - win.matchMedia('(hover: none)').matches ? 'none' : undefined), - ['any-pointer']: (win.matchMedia('(any-pointer: fine)').matches ? 'fine' : - win.matchMedia('(any-pointer: coarse)').matches ? 'coarse' : - win.matchMedia('(any-pointer: none)').matches ? 'none' : undefined), - pointer: (win.matchMedia('(pointer: fine)').matches ? 'fine' : - win.matchMedia('(pointer: coarse)').matches ? 'coarse' : - win.matchMedia('(pointer: none)').matches ? 'none' : undefined), - ['device-aspect-ratio']: (win.matchMedia(`(device-aspect-ratio: ${deviceAspectRatio})`).matches ? deviceAspectRatio : undefined), - ['device-screen']: (win.matchMedia(`(device-width: ${width}px) and (device-height: ${height}px)`).matches ? `${width} x ${height}` : undefined), - ['display-mode']: (win.matchMedia('(display-mode: fullscreen)').matches ? 'fullscreen' : - win.matchMedia('(display-mode: standalone)').matches ? 'standalone' : - win.matchMedia('(display-mode: minimal-ui)').matches ? 'minimal-ui' : - win.matchMedia('(display-mode: browser)').matches ? 'browser' : undefined), - ['color-gamut']: (win.matchMedia('(color-gamut: rec2020)').matches ? 'rec2020' : - win.matchMedia('(color-gamut: p3)').matches ? 'p3' : - win.matchMedia('(color-gamut: srgb)').matches ? 'srgb' : undefined), - orientation: ( - // prefer main window - matchMedia('(orientation: landscape)').matches ? 'landscape' : - matchMedia('(orientation: portrait)').matches ? 'portrait' : undefined), - }; - body.innerHTML = ` - - `; - const style = getComputedStyle(body); - const mediaCSS = { - ['prefers-reduced-motion']: style.getPropertyValue('--prefers-reduced-motion').trim() || undefined, - ['prefers-color-scheme']: style.getPropertyValue('--prefers-color-scheme').trim() || undefined, - monochrome: style.getPropertyValue('--monochrome').trim() || undefined, - ['inverted-colors']: style.getPropertyValue('--inverted-colors').trim() || undefined, - ['forced-colors']: style.getPropertyValue('--forced-colors').trim() || undefined, - ['any-hover']: style.getPropertyValue('--any-hover').trim() || undefined, - hover: style.getPropertyValue('--hover').trim() || undefined, - ['any-pointer']: style.getPropertyValue('--any-pointer').trim() || undefined, - pointer: style.getPropertyValue('--pointer').trim() || undefined, - ['device-aspect-ratio']: style.getPropertyValue('--device-aspect-ratio').trim() || undefined, - ['device-screen']: style.getPropertyValue('--device-screen').trim() || undefined, - ['display-mode']: style.getPropertyValue('--display-mode').trim() || undefined, - ['color-gamut']: style.getPropertyValue('--color-gamut').trim() || undefined, - orientation: style.getPropertyValue('--orientation').trim() || undefined, - }; - // get screen query - const screenQuery = getScreenMedia({ body, width, height }); - logTestResult({ time: timer.stop(), test: 'css media', passed: true }); - return { mediaCSS, matchMediaCSS, screenQuery }; - } - catch (error) { - logTestResult({ test: 'css media', passed: false }); - captureError(error); - return; - } - } - function cssMediaHTML(fp) { - if (!fp.css) { - return ` -
- CSS Media Queries -
@media: ${HTMLNote.BLOCKED}
-
matchMedia: ${HTMLNote.BLOCKED}
-
touch device: ${HTMLNote.BLOCKED}
-
screen query: ${HTMLNote.BLOCKED}
-
`; - } - const { cssMedia: data, } = fp; - const { $hash, mediaCSS, matchMediaCSS, screenQuery, } = data; - return ` -
- ${performanceLogger.getLog()['css media']} - CSS Media Queries${hashSlice($hash)} -
@media: ${!mediaCSS || !Object.keys(mediaCSS).filter((key) => !!mediaCSS[key]).length ? - HTMLNote.BLOCKED : - modal('creep-css-media', `@media

${Object.keys(mediaCSS).map((key) => `${key}: ${mediaCSS[key] || HTMLNote.UNSUPPORTED}`).join('
')}`, hashMini(mediaCSS))}
-
matchMedia: ${!matchMediaCSS || !Object.keys(matchMediaCSS).filter((key) => !!matchMediaCSS[key]).length ? - HTMLNote.BLOCKED : - modal('creep-css-match-media', `matchMedia

${Object.keys(matchMediaCSS).map((key) => `${key}: ${matchMediaCSS[key] || HTMLNote.UNSUPPORTED}`).join('
')}`, hashMini(matchMediaCSS))}
-
touch device: ${!mediaCSS ? HTMLNote.BLOCKED : mediaCSS['any-pointer'] == 'coarse' ? true : HTMLNote.UNKNOWN}
-
screen query: - ${!screenQuery ? HTMLNote.BLOCKED : `${screenQuery.width} x ${screenQuery.height}`} -
-
- `; - } - - function getHTMLElementVersion() { - try { - const timer = createTimer(); - timer.start(); - const keys = []; - // eslint-disable-next-line guard-for-in - for (const key in document.documentElement) { - keys.push(key); - } - logTestResult({ time: timer.stop(), test: 'html element', passed: true }); - return { keys }; - } - catch (error) { - logTestResult({ test: 'html element', passed: false }); - captureError(error); - return; - } - } - function htmlElementVersionHTML(fp) { - if (!fp.htmlElementVersion) { - return ` -
- HTMLElement -
keys (0): ${HTMLNote.Blocked}
-
`; - } - const { htmlElementVersion: { $hash, keys, }, } = fp; - return ` -
- ${performanceLogger.getLog()['html element']} - HTMLElement${hashSlice($hash)} -
keys (${count(keys)}): ${keys && keys.length ? modal('creep-html-element-version', keys.join(', ')) : HTMLNote.Blocked}
-
- `; - } - - function getRectSum(rect) { - return Object.keys(rect).reduce((acc, key) => acc += rect[key], 0) / 100000000; - } - // inspired by - // https://privacycheck.sec.lrz.de/active/fp_gcr/fp_getclientrects.html - // https://privacycheck.sec.lrz.de/active/fp_e/fp_emoji.html - async function getClientRects() { - try { - const timer = createTimer(); - await queueEvent(timer); - const toNativeObject = (domRect) => { - return { - bottom: domRect.bottom, - height: domRect.height, - left: domRect.left, - right: domRect.right, - width: domRect.width, - top: domRect.top, - x: domRect.x, - y: domRect.y, - }; - }; - let lied = (lieProps['Element.getClientRects'] || - lieProps['Element.getBoundingClientRect'] || - lieProps['Range.getClientRects'] || - lieProps['Range.getBoundingClientRect'] || - lieProps['String.fromCodePoint']) || false; - const DOC = (PHANTOM_DARKNESS && - PHANTOM_DARKNESS.document && - PHANTOM_DARKNESS.document.body ? PHANTOM_DARKNESS.document : - document); - const getBestRect = (el) => { - let range; - if (!lieProps['Element.getClientRects']) { - return el.getClientRects()[0]; - } - else if (!lieProps['Element.getBoundingClientRect']) { - return el.getBoundingClientRect(); - } - else if (!lieProps['Range.getClientRects']) { - range = DOC.createRange(); - range.selectNode(el); - return range.getClientRects()[0]; - } - range = DOC.createRange(); - range.selectNode(el); - return range.getBoundingClientRect(); - }; - const rectsId = `${instanceId}-client-rects-div`; - const divElement = document.createElement('div'); - divElement.setAttribute('id', rectsId); - DOC.body.appendChild(divElement); - patch(divElement, html ` -
- -
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - ${EMOJIS.map((emoji) => { - return `
${emoji}
`; - }).join('')} -
-
- `); - // get emoji set and system - const pattern = new Set(); - await queueEvent(timer); - const emojiElems = [...DOC.getElementsByClassName('domrect-emoji')]; - const emojiSet = emojiElems.reduce((emojiSet, el, i) => { - const emoji = EMOJIS[i]; - const { height, width } = getBestRect(el); - const dimensions = `${width},${height}`; - if (!pattern.has(dimensions)) { - pattern.add(dimensions); - emojiSet.add(emoji); - } - return emojiSet; - }, new Set()); - const domrectSystemSum = 0.00001 * [...pattern].map((x) => { - return x.split(',').reduce((acc, x) => acc += (+x || 0), 0); - }).reduce((acc, x) => acc += x, 0); - // get clientRects - const range = document.createRange(); - const rectElems = DOC.getElementsByClassName('rects'); - const elementClientRects = [...rectElems].map((el) => { - return toNativeObject(el.getClientRects()[0]); - }); - const elementBoundingClientRect = [...rectElems].map((el) => { - return toNativeObject(el.getBoundingClientRect()); - }); - const rangeClientRects = [...rectElems].map((el) => { - range.selectNode(el); - return toNativeObject(range.getClientRects()[0]); - }); - const rangeBoundingClientRect = [...rectElems].map((el) => { - range.selectNode(el); - return toNativeObject(range.getBoundingClientRect()); - }); - // detect failed shift calculation - // inspired by https://arkenfox.github.io/TZP - const rect4 = [...rectElems][3]; - const { top: initialTop } = elementClientRects[3]; - rect4.classList.add('shift-dom-rect'); - const { top: shiftedTop } = toNativeObject(rect4.getClientRects()[0]); - rect4.classList.remove('shift-dom-rect'); - const { top: unshiftedTop } = toNativeObject(rect4.getClientRects()[0]); - const diff = initialTop - shiftedTop; - const unshiftLie = diff != (unshiftedTop - shiftedTop); - if (unshiftLie) { - lied = true; - documentLie('Element.getClientRects', 'failed unshift calculation'); - } - // detect failed math calculation lie - let mathLie = false; - elementClientRects.forEach((rect) => { - const { right, left, width, bottom, top, height, x, y } = rect; - if (right - left != width || - bottom - top != height || - right - x != width || - bottom - y != height) { - lied = true; - mathLie = true; - } - return; - }); - if (mathLie) { - documentLie('Element.getClientRects', 'failed math calculation'); - } - // detect equal elements mismatch lie - const { right: right1, left: left1 } = elementClientRects[10]; - const { right: right2, left: left2 } = elementClientRects[11]; - if (right1 != right2 || left1 != left2) { - documentLie('Element.getClientRects', 'equal elements mismatch'); - lied = true; - } - // detect unknown rotate dimensions - const knownEl = [...DOC.getElementsByClassName('rect-known')][0]; - const knownDimensions = toNativeObject(knownEl.getClientRects()[0]); - const knownHash = hashMini(knownDimensions); - if (IS_BLINK) { - if (devicePixelRatio === 1 && knownHash !== '9d9215cc') { - documentLie('Element.getClientRects', 'unknown rotate dimensions'); - lied = true; - } - } - else if (IS_GECKO) { - const Rotate = { - 'e38453f0': true, // 100, etc - }; - if (!Rotate[knownHash]) { - documentLie('Element.getClientRects', 'unknown rotate dimensions'); - lied = true; - } - } - // detect ghost dimensions - const ghostEl = [...DOC.getElementsByClassName('rect-ghost')][0]; - const ghostDimensions = toNativeObject(ghostEl.getClientRects()[0]); - const hasGhostDimensions = Object.keys(ghostDimensions) - .some((key) => ghostDimensions[key] !== 0); - if (hasGhostDimensions) { - documentLie('Element.getClientRects', 'unknown ghost dimensions'); - lied = true; - } - DOC.body.removeChild(DOC.getElementById(rectsId)); - logTestResult({ time: timer.stop(), test: 'rects', passed: true }); - return { - elementClientRects, - elementBoundingClientRect, - rangeClientRects, - rangeBoundingClientRect, - emojiSet: [...emojiSet], - domrectSystemSum, - lied, - }; - } - catch (error) { - logTestResult({ test: 'rects', passed: false }); - captureError(error); - return; - } - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - function clientRectsHTML(fp) { - if (!fp.clientRects) { - return ` -
- DOMRect -
elems A: ${HTMLNote.BLOCKED}
-
elems B: ${HTMLNote.BLOCKED}
-
range A: ${HTMLNote.BLOCKED}
-
range B: ${HTMLNote.BLOCKED}
-
${HTMLNote.BLOCKED}
-
`; - } - const { clientRects: { $hash, elementClientRects, elementBoundingClientRect, rangeClientRects, rangeBoundingClientRect, emojiSet, domrectSystemSum, lied, }, } = fp; - const computeDiffs = (rects) => { - if (!rects || !rects.length) { - return; - } - const expectedSum = rects.reduce((acc, rect) => { - const { right, left, width, bottom, top, height } = rect; - const expected = { - width: right - left, - height: bottom - top, - right: left + width, - left: right - width, - bottom: top + height, - top: bottom - height, - x: right - width, - y: bottom - height, - }; - return acc += getRectSum(expected); - }, 0); - const actualSum = rects.reduce((acc, rect) => acc += getRectSum(rect), 0); - return getDiffs({ - stringA: actualSum, - stringB: expectedSum, - charDiff: true, - decorate: (diff) => `${diff}`, - }); - }; - const helpTitle = `Element.getClientRects()\nhash: ${hashMini(emojiSet)}\n${emojiSet.map((x, i) => i && (i % 6 == 0) ? `${x}\n` : x).join('')}`; - return ` -
- ${performanceLogger.getLog().rects} - DOMRect${hashSlice($hash)} -
elems A: ${computeDiffs(elementClientRects)}
-
elems B: ${computeDiffs(elementBoundingClientRect)}
-
range A: ${computeDiffs(rangeClientRects)}
-
range B: ${computeDiffs(rangeBoundingClientRect)}
-
- ${domrectSystemSum || HTMLNote.UNSUPPORTED} - ${formatEmojiSet(emojiSet)} -
-
- `; - } - - function getErrors(errFns) { - const errors = []; - let i; - const len = errFns.length; - for (i = 0; i < len; i++) { - try { - errFns[i](); - } - catch (err) { - errors.push(err.message); - } - } - return errors; - } - function getConsoleErrors() { - try { - const timer = createTimer(); - timer.start(); - const errorTests = [ - () => new Function('alert(")')(), - () => new Function('const foo;foo.bar')(), - () => new Function('null.bar')(), - () => new Function('abc.xyz = 123')(), - () => new Function('const foo;foo.bar')(), - () => new Function('(1).toString(1000)')(), - () => new Function('[...undefined].length')(), - () => new Function('var x = new Array(-1)')(), - () => new Function('const a=1; const a=2;')(), - ]; - const errors = getErrors(errorTests); - logTestResult({ time: timer.stop(), test: 'console errors', passed: true }); - return { errors }; - } - catch (error) { - logTestResult({ test: 'console errors', passed: false }); - captureError(error); - return; - } - } - function consoleErrorsHTML(fp) { - if (!fp.consoleErrors) { - return ` -
- Error -
results: ${HTMLNote.BLOCKED}
-
`; - } - const { consoleErrors: { $hash, errors, }, } = fp; - const results = Object.keys(errors).map((key) => { - const value = errors[key]; - return `${+key + 1}: ${value}`; - }); - return ` -
- ${performanceLogger.getLog()['console errors']} - Error${hashSlice($hash)} -
results: ${modal('creep-console-errors', results.join('
'))}
-
- `; - } - - /* - Steps to update: - 0. get beta release desktop/mobile - 1. get diffs from template - 2. update feature list - 3. update stable features object - */ - const getStableFeatures = () => ({ - 'Chrome': { - version: 115, - windowKeys: `Object, Function, Array, Number, parseFloat, parseInt, Infinity, NaN, undefined, Boolean, String, Symbol, Date, Promise, RegExp, Error, AggregateError, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, globalThis, JSON, Math, Intl, ArrayBuffer, Atomics, Uint8Array, Int8Array, Uint16Array, Int16Array, Uint32Array, Int32Array, Float32Array, Float64Array, Uint8ClampedArray, BigUint64Array, BigInt64Array, DataView, Map, BigInt, Set, WeakMap, WeakSet, Proxy, Reflect, FinalizationRegistry, WeakRef, decodeURI, decodeURIComponent, encodeURI, encodeURIComponent, escape, unescape, eval, isFinite, isNaN, console, Option, Image, Audio, webkitURL, webkitRTCPeerConnection, webkitMediaStream, WebKitMutationObserver, WebKitCSSMatrix, XSLTProcessor, XPathResult, XPathExpression, XPathEvaluator, XMLSerializer, XMLHttpRequestUpload, XMLHttpRequestEventTarget, XMLHttpRequest, XMLDocument, WritableStreamDefaultWriter, WritableStreamDefaultController, WritableStream, Worker, Window, WheelEvent, WebSocket, WebGLVertexArrayObject, WebGLUniformLocation, WebGLTransformFeedback, WebGLTexture, WebGLSync, WebGLShaderPrecisionFormat, WebGLShader, WebGLSampler, WebGLRenderingContext, WebGLRenderbuffer, WebGLQuery, WebGLProgram, WebGLFramebuffer, WebGLContextEvent, WebGLBuffer, WebGLActiveInfo, WebGL2RenderingContext, WaveShaperNode, VisualViewport, VirtualKeyboardGeometryChangeEvent, ValidityState, VTTCue, UserActivation, URLSearchParams, URLPattern, URL, UIEvent, TrustedTypePolicyFactory, TrustedTypePolicy, TrustedScriptURL, TrustedScript, TrustedHTML, TreeWalker, TransitionEvent, TransformStreamDefaultController, TransformStream, TrackEvent, TouchList, TouchEvent, Touch, TimeRanges, TextTrackList, TextTrackCueList, TextTrackCue, TextTrack, TextMetrics, TextEvent, TextEncoderStream, TextEncoder, TextDecoderStream, TextDecoder, Text, TaskSignal, TaskPriorityChangeEvent, TaskController, TaskAttributionTiming, SyncManager, SubmitEvent, StyleSheetList, StyleSheet, StylePropertyMapReadOnly, StylePropertyMap, StorageEvent, Storage, StereoPannerNode, StaticRange, SourceBufferList, SourceBuffer, ShadowRoot, Selection, SecurityPolicyViolationEvent, ScriptProcessorNode, ScreenOrientation, Screen, Scheduling, Scheduler, SVGViewElement, SVGUseElement, SVGUnitTypes, SVGTransformList, SVGTransform, SVGTitleElement, SVGTextPositioningElement, SVGTextPathElement, SVGTextElement, SVGTextContentElement, SVGTSpanElement, SVGSymbolElement, SVGSwitchElement, SVGStyleElement, SVGStringList, SVGStopElement, SVGSetElement, SVGScriptElement, SVGSVGElement, SVGRectElement, SVGRect, SVGRadialGradientElement, SVGPreserveAspectRatio, SVGPolylineElement, SVGPolygonElement, SVGPointList, SVGPoint, SVGPatternElement, SVGPathElement, SVGNumberList, SVGNumber, SVGMetadataElement, SVGMatrix, SVGMaskElement, SVGMarkerElement, SVGMPathElement, SVGLinearGradientElement, SVGLineElement, SVGLengthList, SVGLength, SVGImageElement, SVGGraphicsElement, SVGGradientElement, SVGGeometryElement, SVGGElement, SVGForeignObjectElement, SVGFilterElement, SVGFETurbulenceElement, SVGFETileElement, SVGFESpotLightElement, SVGFESpecularLightingElement, SVGFEPointLightElement, SVGFEOffsetElement, SVGFEMorphologyElement, SVGFEMergeNodeElement, SVGFEMergeElement, SVGFEImageElement, SVGFEGaussianBlurElement, SVGFEFuncRElement, SVGFEFuncGElement, SVGFEFuncBElement, SVGFEFuncAElement, SVGFEFloodElement, SVGFEDropShadowElement, SVGFEDistantLightElement, SVGFEDisplacementMapElement, SVGFEDiffuseLightingElement, SVGFEConvolveMatrixElement, SVGFECompositeElement, SVGFEComponentTransferElement, SVGFEColorMatrixElement, SVGFEBlendElement, SVGEllipseElement, SVGElement, SVGDescElement, SVGDefsElement, SVGComponentTransferFunctionElement, SVGClipPathElement, SVGCircleElement, SVGAnimationElement, SVGAnimatedTransformList, SVGAnimatedString, SVGAnimatedRect, SVGAnimatedPreserveAspectRatio, SVGAnimatedNumberList, SVGAnimatedNumber, SVGAnimatedLengthList, SVGAnimatedLength, SVGAnimatedInteger, SVGAnimatedEnumeration, SVGAnimatedBoolean, SVGAnimatedAngle, SVGAnimateTransformElement, SVGAnimateMotionElement, SVGAnimateElement, SVGAngle, SVGAElement, Response, ResizeObserverSize, ResizeObserverEntry, ResizeObserver, Request, ReportingObserver, ReadableStreamDefaultReader, ReadableStreamDefaultController, ReadableStreamBYOBRequest, ReadableStreamBYOBReader, ReadableStream, ReadableByteStreamController, Range, RadioNodeList, RTCTrackEvent, RTCStatsReport, RTCSessionDescription, RTCSctpTransport, RTCRtpTransceiver, RTCRtpSender, RTCRtpReceiver, RTCPeerConnectionIceEvent, RTCPeerConnectionIceErrorEvent, RTCPeerConnection, RTCIceTransport, RTCIceCandidate, RTCErrorEvent, RTCError, RTCEncodedVideoFrame, RTCEncodedAudioFrame, RTCDtlsTransport, RTCDataChannelEvent, RTCDataChannel, RTCDTMFToneChangeEvent, RTCDTMFSender, RTCCertificate, PromiseRejectionEvent, ProgressEvent, Profiler, ProcessingInstruction, PopStateEvent, PointerEvent, PluginArray, Plugin, PictureInPictureWindow, PictureInPictureEvent, PeriodicWave, PerformanceTiming, PerformanceServerTiming, PerformanceResourceTiming, PerformancePaintTiming, PerformanceObserverEntryList, PerformanceObserver, PerformanceNavigationTiming, PerformanceNavigation, PerformanceMeasure, PerformanceMark, PerformanceLongTaskTiming, PerformanceEventTiming, PerformanceEntry, PerformanceElementTiming, Performance, Path2D, PannerNode, PageTransitionEvent, OverconstrainedError, OscillatorNode, OffscreenCanvasRenderingContext2D, OffscreenCanvas, OfflineAudioContext, OfflineAudioCompletionEvent, NodeList, NodeIterator, NodeFilter, Node, NetworkInformation, Navigator, NavigationTransition, NavigationHistoryEntry, NavigationDestination, NavigationCurrentEntryChangeEvent, Navigation, NavigateEvent, NamedNodeMap, MutationRecord, MutationObserver, MutationEvent, MouseEvent, MimeTypeArray, MimeType, MessagePort, MessageEvent, MessageChannel, MediaStreamTrackProcessor, MediaStreamTrackGenerator, MediaStreamTrackEvent, MediaStreamTrack, MediaStreamEvent, MediaStreamAudioSourceNode, MediaStreamAudioDestinationNode, MediaStream, MediaSourceHandle, MediaSource, MediaRecorder, MediaQueryListEvent, MediaQueryList, MediaList, MediaError, MediaEncryptedEvent, MediaElementAudioSourceNode, MediaCapabilities, Location, LayoutShiftAttribution, LayoutShift, LargestContentfulPaint, KeyframeEffect, KeyboardEvent, IntersectionObserverEntry, IntersectionObserver, InputEvent, InputDeviceInfo, InputDeviceCapabilities, ImageData, ImageCapture, ImageBitmapRenderingContext, ImageBitmap, IdleDeadline, IIRFilterNode, IDBVersionChangeEvent, IDBTransaction, IDBRequest, IDBOpenDBRequest, IDBObjectStore, IDBKeyRange, IDBIndex, IDBFactory, IDBDatabase, IDBCursorWithValue, IDBCursor, History, Headers, HashChangeEvent, HTMLVideoElement, HTMLUnknownElement, HTMLUListElement, HTMLTrackElement, HTMLTitleElement, HTMLTimeElement, HTMLTextAreaElement, HTMLTemplateElement, HTMLTableSectionElement, HTMLTableRowElement, HTMLTableElement, HTMLTableColElement, HTMLTableCellElement, HTMLTableCaptionElement, HTMLStyleElement, HTMLSpanElement, HTMLSourceElement, HTMLSlotElement, HTMLSelectElement, HTMLScriptElement, HTMLQuoteElement, HTMLProgressElement, HTMLPreElement, HTMLPictureElement, HTMLParamElement, HTMLParagraphElement, HTMLOutputElement, HTMLOptionsCollection, HTMLOptionElement, HTMLOptGroupElement, HTMLObjectElement, HTMLOListElement, HTMLModElement, HTMLMeterElement, HTMLMetaElement, HTMLMenuElement, HTMLMediaElement, HTMLMarqueeElement, HTMLMapElement, HTMLLinkElement, HTMLLegendElement, HTMLLabelElement, HTMLLIElement, HTMLInputElement, HTMLImageElement, HTMLIFrameElement, HTMLHtmlElement, HTMLHeadingElement, HTMLHeadElement, HTMLHRElement, HTMLFrameSetElement, HTMLFrameElement, HTMLFormElement, HTMLFormControlsCollection, HTMLFontElement, HTMLFieldSetElement, HTMLEmbedElement, HTMLElement, HTMLDocument, HTMLDivElement, HTMLDirectoryElement, HTMLDialogElement, HTMLDetailsElement, HTMLDataListElement, HTMLDataElement, HTMLDListElement, HTMLCollection, HTMLCanvasElement, HTMLButtonElement, HTMLBodyElement, HTMLBaseElement, HTMLBRElement, HTMLAudioElement, HTMLAreaElement, HTMLAnchorElement, HTMLAllCollection, GeolocationPositionError, GeolocationPosition, GeolocationCoordinates, Geolocation, GamepadHapticActuator, GamepadEvent, GamepadButton, Gamepad, GainNode, FormDataEvent, FormData, FontFaceSetLoadEvent, FontFace, FocusEvent, FileReader, FileList, File, FeaturePolicy, External, EventTarget, EventSource, EventCounts, Event, ErrorEvent, ElementInternals, Element, DynamicsCompressorNode, DragEvent, DocumentType, DocumentFragment, Document, DelayNode, DecompressionStream, DataTransferItemList, DataTransferItem, DataTransfer, DOMTokenList, DOMStringMap, DOMStringList, DOMRectReadOnly, DOMRectList, DOMRect, DOMQuad, DOMPointReadOnly, DOMPoint, DOMParser, DOMMatrixReadOnly, DOMMatrix, DOMImplementation, DOMException, DOMError, CustomStateSet, CustomEvent, CustomElementRegistry, Crypto, CountQueuingStrategy, ConvolverNode, ConstantSourceNode, CompressionStream, CompositionEvent, Comment, CloseEvent, ClipboardEvent, CharacterData, ChannelSplitterNode, ChannelMergerNode, CanvasRenderingContext2D, CanvasPattern, CanvasGradient, CanvasCaptureMediaStreamTrack, CSSVariableReferenceValue, CSSUnparsedValue, CSSUnitValue, CSSTranslate, CSSTransformValue, CSSTransformComponent, CSSSupportsRule, CSSStyleValue, CSSStyleSheet, CSSStyleRule, CSSStyleDeclaration, CSSSkewY, CSSSkewX, CSSSkew, CSSScale, CSSRuleList, CSSRule, CSSRotate, CSSPropertyRule, CSSPositionValue, CSSPerspective, CSSPageRule, CSSNumericValue, CSSNumericArray, CSSNamespaceRule, CSSMediaRule, CSSMatrixComponent, CSSMathValue, CSSMathSum, CSSMathProduct, CSSMathNegate, CSSMathMin, CSSMathMax, CSSMathInvert, CSSMathClamp, CSSLayerStatementRule, CSSLayerBlockRule, CSSKeywordValue, CSSKeyframesRule, CSSKeyframeRule, CSSImportRule, CSSImageValue, CSSGroupingRule, CSSFontPaletteValuesRule, CSSFontFaceRule, CSSCounterStyleRule, CSSContainerRule, CSSConditionRule, CSS, CDATASection, ByteLengthQueuingStrategy, BroadcastChannel, BlobEvent, Blob, BiquadFilterNode, BeforeUnloadEvent, BeforeInstallPromptEvent, BaseAudioContext, BarProp, AudioWorkletNode, AudioSinkInfo, AudioScheduledSourceNode, AudioProcessingEvent, AudioParamMap, AudioParam, AudioNode, AudioListener, AudioDestinationNode, AudioContext, AudioBufferSourceNode, AudioBuffer, Attr, AnimationEvent, AnimationEffect, Animation, AnalyserNode, AbstractRange, AbortSignal, AbortController, window, self, document, name, location, customElements, history, navigation, locationbar, menubar, personalbar, scrollbars, statusbar, toolbar, status, closed, frames, length, top, opener, parent, frameElement, navigator, origin, external, screen, innerWidth, innerHeight, scrollX, pageXOffset, scrollY, pageYOffset, visualViewport, screenX, screenY, outerWidth, outerHeight, devicePixelRatio, event, clientInformation, offscreenBuffering, screenLeft, screenTop, styleMedia, onsearch, isSecureContext, trustedTypes, performance, onappinstalled, onbeforeinstallprompt, crypto, indexedDB, sessionStorage, localStorage, onbeforexrselect, onabort, onbeforeinput, onblur, oncancel, oncanplay, oncanplaythrough, onchange, onclick, onclose, oncontextlost, oncontextmenu, oncontextrestored, oncuechange, ondblclick, ondrag, ondragend, ondragenter, ondragleave, ondragover, ondragstart, ondrop, ondurationchange, onemptied, onended, onerror, onfocus, onformdata, oninput, oninvalid, onkeydown, onkeypress, onkeyup, onload, onloadeddata, onloadedmetadata, onloadstart, onmousedown, onmouseenter, onmouseleave, onmousemove, onmouseout, onmouseover, onmouseup, onmousewheel, onpause, onplay, onplaying, onprogress, onratechange, onreset, onresize, onscroll, onsecuritypolicyviolation, onseeked, onseeking, onselect, onslotchange, onstalled, onsubmit, onsuspend, ontimeupdate, ontoggle, onvolumechange, onwaiting, onwebkitanimationend, onwebkitanimationiteration, onwebkitanimationstart, onwebkittransitionend, onwheel, onauxclick, ongotpointercapture, onlostpointercapture, onpointerdown, onpointermove, onpointerrawupdate, onpointerup, onpointercancel, onpointerover, onpointerout, onpointerenter, onpointerleave, onselectstart, onselectionchange, onanimationend, onanimationiteration, onanimationstart, ontransitionrun, ontransitionstart, ontransitionend, ontransitioncancel, onafterprint, onbeforeprint, onbeforeunload, onhashchange, onlanguagechange, onmessage, onmessageerror, onoffline, ononline, onpagehide, onpageshow, onpopstate, onrejectionhandled, onstorage, onunhandledrejection, onunload, crossOriginIsolated, scheduler, alert, atob, blur, btoa, cancelAnimationFrame, cancelIdleCallback, captureEvents, clearInterval, clearTimeout, close, confirm, createImageBitmap, fetch, find, focus, getComputedStyle, getSelection, matchMedia, moveBy, moveTo, open, postMessage, print, prompt, queueMicrotask, releaseEvents, reportError, requestAnimationFrame, requestIdleCallback, resizeBy, resizeTo, scroll, scrollBy, scrollTo, setInterval, setTimeout, stop, structuredClone, webkitCancelAnimationFrame, webkitRequestAnimationFrame, chrome, WebAssembly, credentialless, caches, cookieStore, ondevicemotion, ondeviceorientation, ondeviceorientationabsolute, launchQueue, onbeforematch, onbeforetoggle, AbsoluteOrientationSensor, Accelerometer, AudioWorklet, BatteryManager, Cache, CacheStorage, Clipboard, ClipboardItem, CookieChangeEvent, CookieStore, CookieStoreManager, Credential, CredentialsContainer, CryptoKey, DeviceMotionEvent, DeviceMotionEventAcceleration, DeviceMotionEventRotationRate, DeviceOrientationEvent, FederatedCredential, GravitySensor, Gyroscope, Keyboard, KeyboardLayoutMap, LinearAccelerationSensor, Lock, LockManager, MIDIAccess, MIDIConnectionEvent, MIDIInput, MIDIInputMap, MIDIMessageEvent, MIDIOutput, MIDIOutputMap, MIDIPort, MediaDeviceInfo, MediaDevices, MediaKeyMessageEvent, MediaKeySession, MediaKeyStatusMap, MediaKeySystemAccess, MediaKeys, NavigationPreloadManager, NavigatorManagedData, OrientationSensor, PasswordCredential, RelativeOrientationSensor, Sanitizer, ScreenDetailed, ScreenDetails, Sensor, SensorErrorEvent, ServiceWorker, ServiceWorkerContainer, ServiceWorkerRegistration, StorageManager, SubtleCrypto, VirtualKeyboard, WebTransport, WebTransportBidirectionalStream, WebTransportDatagramDuplexStream, WebTransportError, Worklet, XRDOMOverlayState, XRLayer, XRWebGLBinding, AudioData, EncodedAudioChunk, EncodedVideoChunk, ImageTrack, ImageTrackList, VideoColorSpace, VideoFrame, AudioDecoder, AudioEncoder, ImageDecoder, VideoDecoder, VideoEncoder, AuthenticatorAssertionResponse, AuthenticatorAttestationResponse, AuthenticatorResponse, PublicKeyCredential, Bluetooth, BluetoothCharacteristicProperties, BluetoothDevice, BluetoothRemoteGATTCharacteristic, BluetoothRemoteGATTDescriptor, BluetoothRemoteGATTServer, BluetoothRemoteGATTService, CaptureController, EyeDropper, FileSystemDirectoryHandle, FileSystemFileHandle, FileSystemHandle, FileSystemWritableFileStream, FontData, FragmentDirective, GPU, GPUAdapter, GPUAdapterInfo, GPUBindGroup, GPUBindGroupLayout, GPUBuffer, GPUBufferUsage, GPUCanvasContext, GPUColorWrite, GPUCommandBuffer, GPUCommandEncoder, GPUCompilationInfo, GPUCompilationMessage, GPUComputePassEncoder, GPUComputePipeline, GPUDevice, GPUDeviceLostInfo, GPUError, GPUExternalTexture, GPUInternalError, GPUMapMode, GPUOutOfMemoryError, GPUPipelineError, GPUPipelineLayout, GPUQuerySet, GPUQueue, GPURenderBundle, GPURenderBundleEncoder, GPURenderPassEncoder, GPURenderPipeline, GPUSampler, GPUShaderModule, GPUShaderStage, GPUSupportedFeatures, GPUSupportedLimits, GPUTexture, GPUTextureUsage, GPUTextureView, GPUUncapturedErrorEvent, GPUValidationError, HID, HIDConnectionEvent, HIDDevice, HIDInputReportEvent, IdentityCredential, IdleDetector, LaunchParams, LaunchQueue, OTPCredential, PaymentAddress, PaymentRequest, PaymentResponse, PaymentMethodChangeEvent, Presentation, PresentationAvailability, PresentationConnection, PresentationConnectionAvailableEvent, PresentationConnectionCloseEvent, PresentationConnectionList, PresentationReceiver, PresentationRequest, Serial, SerialPort, ToggleEvent, USB, USBAlternateInterface, USBConfiguration, USBConnectionEvent, USBDevice, USBEndpoint, USBInTransferResult, USBInterface, USBIsochronousInTransferPacket, USBIsochronousInTransferResult, USBIsochronousOutTransferPacket, USBIsochronousOutTransferResult, USBOutTransferResult, WakeLock, WakeLockSentinel, WindowControlsOverlay, WindowControlsOverlayGeometryChangeEvent, XRAnchor, XRAnchorSet, XRBoundedReferenceSpace, XRCPUDepthInformation, XRCamera, XRDepthInformation, XRFrame, XRHitTestResult, XRHitTestSource, XRInputSource, XRInputSourceArray, XRInputSourceEvent, XRInputSourcesChangeEvent, XRLightEstimate, XRLightProbe, XRPose, XRRay, XRReferenceSpace, XRReferenceSpaceEvent, XRRenderState, XRRigidTransform, XRSession, XRSessionEvent, XRSpace, XRSystem, XRTransientInputHitTestResult, XRTransientInputHitTestSource, XRView, XRViewerPose, XRViewport, XRWebGLDepthInformation, XRWebGLLayer, getScreenDetails, queryLocalFonts, showDirectoryPicker, showOpenFilePicker, showSaveFilePicker, originAgentCluster, speechSynthesis, oncontentvisibilityautostatechange, onscrollend, AnimationPlaybackEvent, AnimationTimeline, CSSAnimation, CSSTransition, DocumentTimeline, BackgroundFetchManager, BackgroundFetchRecord, BackgroundFetchRegistration, BluetoothUUID, BrowserCaptureMediaStreamTrack, CropTarget, ContentVisibilityAutoStateChangeEvent, DelegatedInkTrailPresenter, Ink, Highlight, HighlightRegistry, MathMLElement, MediaMetadata, MediaSession, NavigatorUAData, Notification, PaymentManager, PaymentRequestUpdateEvent, PeriodicSyncManager, PermissionStatus, Permissions, PushManager, PushSubscription, PushSubscriptionOptions, RemotePlayback, SharedWorker, SpeechSynthesisErrorEvent, SpeechSynthesisEvent, SpeechSynthesisUtterance, VideoPlaybackQuality, ViewTransition, webkitSpeechGrammar, webkitSpeechGrammarList, webkitSpeechRecognition, webkitSpeechRecognitionError, webkitSpeechRecognitionEvent, openDatabase, webkitRequestFileSystem, webkitResolveLocalFileSystemURL`, - cssKeys: `cssText, length, parentRule, cssFloat, getPropertyPriority, getPropertyValue, item, removeProperty, setProperty, constructor, accent-color, align-content, align-items, align-self, alignment-baseline, animation-composition, animation-delay, animation-direction, animation-duration, animation-fill-mode, animation-iteration-count, animation-name, animation-play-state, animation-timing-function, app-region, appearance, backdrop-filter, backface-visibility, background-attachment, background-blend-mode, background-clip, background-color, background-image, background-origin, background-position, background-repeat, background-size, baseline-shift, baseline-source, block-size, border-block-end-color, border-block-end-style, border-block-end-width, border-block-start-color, border-block-start-style, border-block-start-width, border-bottom-color, border-bottom-left-radius, border-bottom-right-radius, border-bottom-style, border-bottom-width, border-collapse, border-end-end-radius, border-end-start-radius, border-image-outset, border-image-repeat, border-image-slice, border-image-source, border-image-width, border-inline-end-color, border-inline-end-style, border-inline-end-width, border-inline-start-color, border-inline-start-style, border-inline-start-width, border-left-color, border-left-style, border-left-width, border-right-color, border-right-style, border-right-width, border-start-end-radius, border-start-start-radius, border-top-color, border-top-left-radius, border-top-right-radius, border-top-style, border-top-width, bottom, box-shadow, box-sizing, break-after, break-before, break-inside, buffered-rendering, caption-side, caret-color, clear, clip, clip-path, clip-rule, color, color-interpolation, color-interpolation-filters, color-rendering, column-count, column-gap, column-rule-color, column-rule-style, column-rule-width, column-span, column-width, contain-intrinsic-block-size, contain-intrinsic-height, contain-intrinsic-inline-size, contain-intrinsic-size, contain-intrinsic-width, container-name, container-type, content, cursor, cx, cy, d, direction, display, dominant-baseline, empty-cells, fill, fill-opacity, fill-rule, filter, flex-basis, flex-direction, flex-grow, flex-shrink, flex-wrap, float, flood-color, flood-opacity, font-family, font-kerning, font-optical-sizing, font-palette, font-size, font-stretch, font-style, font-synthesis-small-caps, font-synthesis-style, font-synthesis-weight, font-variant, font-variant-alternates, font-variant-caps, font-variant-east-asian, font-variant-ligatures, font-variant-numeric, font-weight, grid-auto-columns, grid-auto-flow, grid-auto-rows, grid-column-end, grid-column-start, grid-row-end, grid-row-start, grid-template-areas, grid-template-columns, grid-template-rows, height, hyphenate-character, hyphenate-limit-chars, hyphens, image-orientation, image-rendering, initial-letter, inline-size, inset-block-end, inset-block-start, inset-inline-end, inset-inline-start, isolation, justify-content, justify-items, justify-self, left, letter-spacing, lighting-color, line-break, line-height, list-style-image, list-style-position, list-style-type, margin-block-end, margin-block-start, margin-bottom, margin-inline-end, margin-inline-start, margin-left, margin-right, margin-top, marker-end, marker-mid, marker-start, mask-type, math-depth, math-shift, math-style, max-block-size, max-height, max-inline-size, max-width, min-block-size, min-height, min-inline-size, min-width, mix-blend-mode, object-fit, object-position, object-view-box, offset-distance, offset-path, offset-rotate, opacity, order, orphans, outline-color, outline-offset, outline-style, outline-width, overflow-anchor, overflow-clip-margin, overflow-wrap, overflow-x, overflow-y, overscroll-behavior-block, overscroll-behavior-inline, padding-block-end, padding-block-start, padding-bottom, padding-inline-end, padding-inline-start, padding-left, padding-right, padding-top, paint-order, perspective, perspective-origin, pointer-events, position, r, resize, right, rotate, row-gap, ruby-position, rx, ry, scale, scroll-behavior, scroll-margin-block-end, scroll-margin-block-start, scroll-margin-inline-end, scroll-margin-inline-start, scroll-padding-block-end, scroll-padding-block-start, scroll-padding-inline-end, scroll-padding-inline-start, scrollbar-gutter, shape-image-threshold, shape-margin, shape-outside, shape-rendering, speak, stop-color, stop-opacity, stroke, stroke-dasharray, stroke-dashoffset, stroke-linecap, stroke-linejoin, stroke-miterlimit, stroke-opacity, stroke-width, tab-size, table-layout, text-align, text-align-last, text-anchor, text-decoration, text-decoration-color, text-decoration-line, text-decoration-skip-ink, text-decoration-style, text-emphasis-color, text-emphasis-position, text-emphasis-style, text-indent, text-overflow, text-rendering, text-shadow, text-size-adjust, text-transform, text-underline-position, text-wrap, top, touch-action, transform, transform-origin, transform-style, transition-delay, transition-duration, transition-property, transition-timing-function, translate, unicode-bidi, user-select, vector-effect, vertical-align, view-transition-name, visibility, white-space-collapse, widows, width, will-change, word-break, word-spacing, writing-mode, x, y, z-index, zoom, -webkit-border-horizontal-spacing, -webkit-border-image, -webkit-border-vertical-spacing, -webkit-box-align, -webkit-box-decoration-break, -webkit-box-direction, -webkit-box-flex, -webkit-box-ordinal-group, -webkit-box-orient, -webkit-box-pack, -webkit-box-reflect, -webkit-font-smoothing, -webkit-highlight, -webkit-line-break, -webkit-line-clamp, -webkit-locale, -webkit-mask-box-image, -webkit-mask-box-image-outset, -webkit-mask-box-image-repeat, -webkit-mask-box-image-slice, -webkit-mask-box-image-source, -webkit-mask-box-image-width, -webkit-mask-clip, -webkit-mask-composite, -webkit-mask-image, -webkit-mask-origin, -webkit-mask-position, -webkit-mask-repeat, -webkit-mask-size, -webkit-print-color-adjust, -webkit-rtl-ordering, -webkit-tap-highlight-color, -webkit-text-combine, -webkit-text-decorations-in-effect, -webkit-text-fill-color, -webkit-text-orientation, -webkit-text-security, -webkit-text-stroke-color, -webkit-text-stroke-width, -webkit-user-drag, -webkit-user-modify, -webkit-writing-mode, accentColor, additiveSymbols, alignContent, alignItems, alignSelf, alignmentBaseline, all, animation, animationComposition, animationDelay, animationDirection, animationDuration, animationFillMode, animationIterationCount, animationName, animationPlayState, animationTimingFunction, appRegion, ascentOverride, aspectRatio, backdropFilter, backfaceVisibility, background, backgroundAttachment, backgroundBlendMode, backgroundClip, backgroundColor, backgroundImage, backgroundOrigin, backgroundPosition, backgroundPositionX, backgroundPositionY, backgroundRepeat, backgroundRepeatX, backgroundRepeatY, backgroundSize, basePalette, baselineShift, baselineSource, blockSize, border, borderBlock, borderBlockColor, borderBlockEnd, borderBlockEndColor, borderBlockEndStyle, borderBlockEndWidth, borderBlockStart, borderBlockStartColor, borderBlockStartStyle, borderBlockStartWidth, borderBlockStyle, borderBlockWidth, borderBottom, borderBottomColor, borderBottomLeftRadius, borderBottomRightRadius, borderBottomStyle, borderBottomWidth, borderCollapse, borderColor, borderEndEndRadius, borderEndStartRadius, borderImage, borderImageOutset, borderImageRepeat, borderImageSlice, borderImageSource, borderImageWidth, borderInline, borderInlineColor, borderInlineEnd, borderInlineEndColor, borderInlineEndStyle, borderInlineEndWidth, borderInlineStart, borderInlineStartColor, borderInlineStartStyle, borderInlineStartWidth, borderInlineStyle, borderInlineWidth, borderLeft, borderLeftColor, borderLeftStyle, borderLeftWidth, borderRadius, borderRight, borderRightColor, borderRightStyle, borderRightWidth, borderSpacing, borderStartEndRadius, borderStartStartRadius, borderStyle, borderTop, borderTopColor, borderTopLeftRadius, borderTopRightRadius, borderTopStyle, borderTopWidth, borderWidth, boxShadow, boxSizing, breakAfter, breakBefore, breakInside, bufferedRendering, captionSide, caretColor, clipPath, clipRule, colorInterpolation, colorInterpolationFilters, colorRendering, colorScheme, columnCount, columnFill, columnGap, columnRule, columnRuleColor, columnRuleStyle, columnRuleWidth, columnSpan, columnWidth, columns, contain, containIntrinsicBlockSize, containIntrinsicHeight, containIntrinsicInlineSize, containIntrinsicSize, containIntrinsicWidth, container, containerName, containerType, contentVisibility, counterIncrement, counterReset, counterSet, descentOverride, dominantBaseline, emptyCells, fallback, fillOpacity, fillRule, flex, flexBasis, flexDirection, flexFlow, flexGrow, flexShrink, flexWrap, floodColor, floodOpacity, font, fontDisplay, fontFamily, fontFeatureSettings, fontKerning, fontOpticalSizing, fontPalette, fontSize, fontStretch, fontStyle, fontSynthesis, fontSynthesisSmallCaps, fontSynthesisStyle, fontSynthesisWeight, fontVariant, fontVariantAlternates, fontVariantCaps, fontVariantEastAsian, fontVariantLigatures, fontVariantNumeric, fontVariationSettings, fontWeight, forcedColorAdjust, gap, grid, gridArea, gridAutoColumns, gridAutoFlow, gridAutoRows, gridColumn, gridColumnEnd, gridColumnGap, gridColumnStart, gridGap, gridRow, gridRowEnd, gridRowGap, gridRowStart, gridTemplate, gridTemplateAreas, gridTemplateColumns, gridTemplateRows, hyphenateCharacter, hyphenateLimitChars, imageOrientation, imageRendering, inherits, initialLetter, initialValue, inlineSize, inset, insetBlock, insetBlockEnd, insetBlockStart, insetInline, insetInlineEnd, insetInlineStart, justifyContent, justifyItems, justifySelf, letterSpacing, lightingColor, lineBreak, lineGapOverride, lineHeight, listStyle, listStyleImage, listStylePosition, listStyleType, margin, marginBlock, marginBlockEnd, marginBlockStart, marginBottom, marginInline, marginInlineEnd, marginInlineStart, marginLeft, marginRight, marginTop, marker, markerEnd, markerMid, markerStart, mask, maskType, mathDepth, mathShift, mathStyle, maxBlockSize, maxHeight, maxInlineSize, maxWidth, minBlockSize, minHeight, minInlineSize, minWidth, mixBlendMode, negative, objectFit, objectPosition, objectViewBox, offset, offsetDistance, offsetPath, offsetRotate, outline, outlineColor, outlineOffset, outlineStyle, outlineWidth, overflow, overflowAnchor, overflowClipMargin, overflowWrap, overflowX, overflowY, overrideColors, overscrollBehavior, overscrollBehaviorBlock, overscrollBehaviorInline, overscrollBehaviorX, overscrollBehaviorY, pad, padding, paddingBlock, paddingBlockEnd, paddingBlockStart, paddingBottom, paddingInline, paddingInlineEnd, paddingInlineStart, paddingLeft, paddingRight, paddingTop, page, pageBreakAfter, pageBreakBefore, pageBreakInside, pageOrientation, paintOrder, perspectiveOrigin, placeContent, placeItems, placeSelf, pointerEvents, prefix, quotes, range, rowGap, rubyPosition, scrollBehavior, scrollMargin, scrollMarginBlock, scrollMarginBlockEnd, scrollMarginBlockStart, scrollMarginBottom, scrollMarginInline, scrollMarginInlineEnd, scrollMarginInlineStart, scrollMarginLeft, scrollMarginRight, scrollMarginTop, scrollPadding, scrollPaddingBlock, scrollPaddingBlockEnd, scrollPaddingBlockStart, scrollPaddingBottom, scrollPaddingInline, scrollPaddingInlineEnd, scrollPaddingInlineStart, scrollPaddingLeft, scrollPaddingRight, scrollPaddingTop, scrollSnapAlign, scrollSnapStop, scrollSnapType, scrollbarGutter, shapeImageThreshold, shapeMargin, shapeOutside, shapeRendering, size, sizeAdjust, speakAs, src, stopColor, stopOpacity, strokeDasharray, strokeDashoffset, strokeLinecap, strokeLinejoin, strokeMiterlimit, strokeOpacity, strokeWidth, suffix, symbols, syntax, system, tabSize, tableLayout, textAlign, textAlignLast, textAnchor, textCombineUpright, textDecoration, textDecorationColor, textDecorationLine, textDecorationSkipInk, textDecorationStyle, textDecorationThickness, textEmphasis, textEmphasisColor, textEmphasisPosition, textEmphasisStyle, textIndent, textOrientation, textOverflow, textRendering, textShadow, textSizeAdjust, textTransform, textUnderlineOffset, textUnderlinePosition, textWrap, touchAction, transformBox, transformOrigin, transformStyle, transition, transitionDelay, transitionDuration, transitionProperty, transitionTimingFunction, unicodeBidi, unicodeRange, userSelect, vectorEffect, verticalAlign, viewTransitionName, webkitAlignContent, webkitAlignItems, webkitAlignSelf, webkitAnimation, webkitAnimationDelay, webkitAnimationDirection, webkitAnimationDuration, webkitAnimationFillMode, webkitAnimationIterationCount, webkitAnimationName, webkitAnimationPlayState, webkitAnimationTimingFunction, webkitAppRegion, webkitAppearance, webkitBackfaceVisibility, webkitBackgroundClip, webkitBackgroundOrigin, webkitBackgroundSize, webkitBorderAfter, webkitBorderAfterColor, webkitBorderAfterStyle, webkitBorderAfterWidth, webkitBorderBefore, webkitBorderBeforeColor, webkitBorderBeforeStyle, webkitBorderBeforeWidth, webkitBorderBottomLeftRadius, webkitBorderBottomRightRadius, webkitBorderEnd, webkitBorderEndColor, webkitBorderEndStyle, webkitBorderEndWidth, webkitBorderHorizontalSpacing, webkitBorderImage, webkitBorderRadius, webkitBorderStart, webkitBorderStartColor, webkitBorderStartStyle, webkitBorderStartWidth, webkitBorderTopLeftRadius, webkitBorderTopRightRadius, webkitBorderVerticalSpacing, webkitBoxAlign, webkitBoxDecorationBreak, webkitBoxDirection, webkitBoxFlex, webkitBoxOrdinalGroup, webkitBoxOrient, webkitBoxPack, webkitBoxReflect, webkitBoxShadow, webkitBoxSizing, webkitClipPath, webkitColumnBreakAfter, webkitColumnBreakBefore, webkitColumnBreakInside, webkitColumnCount, webkitColumnGap, webkitColumnRule, webkitColumnRuleColor, webkitColumnRuleStyle, webkitColumnRuleWidth, webkitColumnSpan, webkitColumnWidth, webkitColumns, webkitFilter, webkitFlex, webkitFlexBasis, webkitFlexDirection, webkitFlexFlow, webkitFlexGrow, webkitFlexShrink, webkitFlexWrap, webkitFontFeatureSettings, webkitFontSmoothing, webkitHighlight, webkitHyphenateCharacter, webkitJustifyContent, webkitLineBreak, webkitLineClamp, webkitLocale, webkitLogicalHeight, webkitLogicalWidth, webkitMarginAfter, webkitMarginBefore, webkitMarginEnd, webkitMarginStart, webkitMask, webkitMaskBoxImage, webkitMaskBoxImageOutset, webkitMaskBoxImageRepeat, webkitMaskBoxImageSlice, webkitMaskBoxImageSource, webkitMaskBoxImageWidth, webkitMaskClip, webkitMaskComposite, webkitMaskImage, webkitMaskOrigin, webkitMaskPosition, webkitMaskPositionX, webkitMaskPositionY, webkitMaskRepeat, webkitMaskRepeatX, webkitMaskRepeatY, webkitMaskSize, webkitMaxLogicalHeight, webkitMaxLogicalWidth, webkitMinLogicalHeight, webkitMinLogicalWidth, webkitOpacity, webkitOrder, webkitPaddingAfter, webkitPaddingBefore, webkitPaddingEnd, webkitPaddingStart, webkitPerspective, webkitPerspectiveOrigin, webkitPerspectiveOriginX, webkitPerspectiveOriginY, webkitPrintColorAdjust, webkitRtlOrdering, webkitRubyPosition, webkitShapeImageThreshold, webkitShapeMargin, webkitShapeOutside, webkitTapHighlightColor, webkitTextCombine, webkitTextDecorationsInEffect, webkitTextEmphasis, webkitTextEmphasisColor, webkitTextEmphasisPosition, webkitTextEmphasisStyle, webkitTextFillColor, webkitTextOrientation, webkitTextSecurity, webkitTextSizeAdjust, webkitTextStroke, webkitTextStrokeColor, webkitTextStrokeWidth, webkitTransform, webkitTransformOrigin, webkitTransformOriginX, webkitTransformOriginY, webkitTransformOriginZ, webkitTransformStyle, webkitTransition, webkitTransitionDelay, webkitTransitionDuration, webkitTransitionProperty, webkitTransitionTimingFunction, webkitUserDrag, webkitUserModify, webkitUserSelect, webkitWritingMode, whiteSpace, whiteSpaceCollapse, willChange, wordBreak, wordSpacing, wordWrap, writingMode, zIndex, additive-symbols, ascent-override, aspect-ratio, background-position-x, background-position-y, background-repeat-x, background-repeat-y, base-palette, border-block, border-block-color, border-block-end, border-block-start, border-block-style, border-block-width, border-bottom, border-color, border-image, border-inline, border-inline-color, border-inline-end, border-inline-start, border-inline-style, border-inline-width, border-left, border-radius, border-right, border-spacing, border-style, border-top, border-width, color-scheme, column-fill, column-rule, content-visibility, counter-increment, counter-reset, counter-set, descent-override, flex-flow, font-display, font-feature-settings, font-synthesis, font-variation-settings, forced-color-adjust, grid-area, grid-column, grid-column-gap, grid-gap, grid-row, grid-row-gap, grid-template, initial-value, inset-block, inset-inline, line-gap-override, list-style, margin-block, margin-inline, override-colors, overscroll-behavior, overscroll-behavior-x, overscroll-behavior-y, padding-block, padding-inline, page-break-after, page-break-before, page-break-inside, page-orientation, place-content, place-items, place-self, scroll-margin, scroll-margin-block, scroll-margin-bottom, scroll-margin-inline, scroll-margin-left, scroll-margin-right, scroll-margin-top, scroll-padding, scroll-padding-block, scroll-padding-bottom, scroll-padding-inline, scroll-padding-left, scroll-padding-right, scroll-padding-top, scroll-snap-align, scroll-snap-stop, scroll-snap-type, size-adjust, speak-as, text-combine-upright, text-decoration-thickness, text-emphasis, text-orientation, text-underline-offset, transform-box, unicode-range, -webkit-align-content, -webkit-align-items, -webkit-align-self, -webkit-animation, -webkit-animation-delay, -webkit-animation-direction, -webkit-animation-duration, -webkit-animation-fill-mode, -webkit-animation-iteration-count, -webkit-animation-name, -webkit-animation-play-state, -webkit-animation-timing-function, -webkit-app-region, -webkit-appearance, -webkit-backface-visibility, -webkit-background-clip, -webkit-background-origin, -webkit-background-size, -webkit-border-after, -webkit-border-after-color, -webkit-border-after-style, -webkit-border-after-width, -webkit-border-before, -webkit-border-before-color, -webkit-border-before-style, -webkit-border-before-width, -webkit-border-bottom-left-radius, -webkit-border-bottom-right-radius, -webkit-border-end, -webkit-border-end-color, -webkit-border-end-style, -webkit-border-end-width, -webkit-border-radius, -webkit-border-start, -webkit-border-start-color, -webkit-border-start-style, -webkit-border-start-width, -webkit-border-top-left-radius, -webkit-border-top-right-radius, -webkit-box-shadow, -webkit-box-sizing, -webkit-clip-path, -webkit-column-break-after, -webkit-column-break-before, -webkit-column-break-inside, -webkit-column-count, -webkit-column-gap, -webkit-column-rule, -webkit-column-rule-color, -webkit-column-rule-style, -webkit-column-rule-width, -webkit-column-span, -webkit-column-width, -webkit-columns, -webkit-filter, -webkit-flex, -webkit-flex-basis, -webkit-flex-direction, -webkit-flex-flow, -webkit-flex-grow, -webkit-flex-shrink, -webkit-flex-wrap, -webkit-font-feature-settings, -webkit-hyphenate-character, -webkit-justify-content, -webkit-logical-height, -webkit-logical-width, -webkit-margin-after, -webkit-margin-before, -webkit-margin-end, -webkit-margin-start, -webkit-mask, -webkit-mask-position-x, -webkit-mask-position-y, -webkit-mask-repeat-x, -webkit-mask-repeat-y, -webkit-max-logical-height, -webkit-max-logical-width, -webkit-min-logical-height, -webkit-min-logical-width, -webkit-opacity, -webkit-order, -webkit-padding-after, -webkit-padding-before, -webkit-padding-end, -webkit-padding-start, -webkit-perspective, -webkit-perspective-origin, -webkit-perspective-origin-x, -webkit-perspective-origin-y, -webkit-ruby-position, -webkit-shape-image-threshold, -webkit-shape-margin, -webkit-shape-outside, -webkit-text-emphasis, -webkit-text-emphasis-color, -webkit-text-emphasis-position, -webkit-text-emphasis-style, -webkit-text-size-adjust, -webkit-text-stroke, -webkit-transform, -webkit-transform-origin, -webkit-transform-origin-x, -webkit-transform-origin-y, -webkit-transform-origin-z, -webkit-transform-style, -webkit-transition, -webkit-transition-delay, -webkit-transition-duration, -webkit-transition-property, -webkit-transition-timing-function, -webkit-user-select, white-space, word-wrap`, - jsKeys: 'Object.assign, Object.getOwnPropertyDescriptor, Object.getOwnPropertyDescriptors, Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.hasOwn, Object.is, Object.preventExtensions, Object.seal, Object.create, Object.defineProperties, Object.defineProperty, Object.freeze, Object.getPrototypeOf, Object.setPrototypeOf, Object.isExtensible, Object.isFrozen, Object.isSealed, Object.keys, Object.entries, Object.fromEntries, Object.values, Object.__defineGetter__, Object.__defineSetter__, Object.hasOwnProperty, Object.__lookupGetter__, Object.__lookupSetter__, Object.isPrototypeOf, Object.propertyIsEnumerable, Object.toString, Object.valueOf, Object.__proto__, Object.toLocaleString, Function.apply, Function.bind, Function.call, Function.toString, Boolean.toString, Boolean.valueOf, Symbol.for, Symbol.keyFor, Symbol.asyncIterator, Symbol.hasInstance, Symbol.isConcatSpreadable, Symbol.iterator, Symbol.match, Symbol.matchAll, Symbol.replace, Symbol.search, Symbol.species, Symbol.split, Symbol.toPrimitive, Symbol.toStringTag, Symbol.unscopables, Symbol.toString, Symbol.valueOf, Symbol.description, Error.captureStackTrace, Error.stackTraceLimit, Error.message, Error.toString, Number.isFinite, Number.isInteger, Number.isNaN, Number.isSafeInteger, Number.parseFloat, Number.parseInt, Number.MAX_VALUE, Number.MIN_VALUE, Number.NaN, Number.NEGATIVE_INFINITY, Number.POSITIVE_INFINITY, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, Number.EPSILON, Number.toExponential, Number.toFixed, Number.toPrecision, Number.toString, Number.valueOf, Number.toLocaleString, BigInt.asUintN, BigInt.asIntN, BigInt.toLocaleString, BigInt.toString, BigInt.valueOf, Math.abs, Math.acos, Math.acosh, Math.asin, Math.asinh, Math.atan, Math.atanh, Math.atan2, Math.ceil, Math.cbrt, Math.expm1, Math.clz32, Math.cos, Math.cosh, Math.exp, Math.floor, Math.fround, Math.hypot, Math.imul, Math.log, Math.log1p, Math.log2, Math.log10, Math.max, Math.min, Math.pow, Math.random, Math.round, Math.sign, Math.sin, Math.sinh, Math.sqrt, Math.tan, Math.tanh, Math.trunc, Math.E, Math.LN10, Math.LN2, Math.LOG10E, Math.LOG2E, Math.PI, Math.SQRT1_2, Math.SQRT2, Date.now, Date.parse, Date.UTC, Date.toString, Date.toDateString, Date.toTimeString, Date.toISOString, Date.toUTCString, Date.toGMTString, Date.getDate, Date.setDate, Date.getDay, Date.getFullYear, Date.setFullYear, Date.getHours, Date.setHours, Date.getMilliseconds, Date.setMilliseconds, Date.getMinutes, Date.setMinutes, Date.getMonth, Date.setMonth, Date.getSeconds, Date.setSeconds, Date.getTime, Date.setTime, Date.getTimezoneOffset, Date.getUTCDate, Date.setUTCDate, Date.getUTCDay, Date.getUTCFullYear, Date.setUTCFullYear, Date.getUTCHours, Date.setUTCHours, Date.getUTCMilliseconds, Date.setUTCMilliseconds, Date.getUTCMinutes, Date.setUTCMinutes, Date.getUTCMonth, Date.setUTCMonth, Date.getUTCSeconds, Date.setUTCSeconds, Date.valueOf, Date.getYear, Date.setYear, Date.toJSON, Date.toLocaleString, Date.toLocaleDateString, Date.toLocaleTimeString, String.fromCharCode, String.fromCodePoint, String.raw, String.anchor, String.at, String.big, String.blink, String.bold, String.charAt, String.charCodeAt, String.codePointAt, String.concat, String.endsWith, String.fontcolor, String.fontsize, String.fixed, String.includes, String.indexOf, String.italics, String.lastIndexOf, String.link, String.localeCompare, String.match, String.matchAll, String.normalize, String.padEnd, String.padStart, String.repeat, String.replace, String.replaceAll, String.search, String.slice, String.small, String.split, String.strike, String.sub, String.substr, String.substring, String.sup, String.startsWith, String.toString, String.trim, String.trimStart, String.trimLeft, String.trimEnd, String.trimRight, String.toLocaleLowerCase, String.toLocaleUpperCase, String.toLowerCase, String.toUpperCase, String.valueOf, String.isWellFormed, String.toWellFormed, RegExp.input, RegExp.$_, RegExp.lastMatch, RegExp.$&, RegExp.lastParen, RegExp.$+, RegExp.leftContext, RegExp.$`, RegExp.rightContext, RegExp.$\', RegExp.$1, RegExp.$2, RegExp.$3, RegExp.$4, RegExp.$5, RegExp.$6, RegExp.$7, RegExp.$8, RegExp.$9, RegExp.exec, RegExp.dotAll, RegExp.flags, RegExp.global, RegExp.hasIndices, RegExp.ignoreCase, RegExp.multiline, RegExp.source, RegExp.sticky, RegExp.unicode, RegExp.compile, RegExp.toString, RegExp.test, RegExp.unicodeSets, Array.isArray, Array.from, Array.of, Array.at, Array.concat, Array.copyWithin, Array.fill, Array.find, Array.findIndex, Array.findLast, Array.findLastIndex, Array.lastIndexOf, Array.pop, Array.push, Array.reverse, Array.shift, Array.unshift, Array.slice, Array.sort, Array.splice, Array.includes, Array.indexOf, Array.join, Array.keys, Array.entries, Array.values, Array.forEach, Array.filter, Array.flat, Array.flatMap, Array.map, Array.every, Array.some, Array.reduce, Array.reduceRight, Array.toLocaleString, Array.toString, Array.toReversed, Array.toSorted, Array.toSpliced, Array.with, Map.get, Map.set, Map.has, Map.delete, Map.clear, Map.entries, Map.forEach, Map.keys, Map.size, Map.values, Set.has, Set.add, Set.delete, Set.clear, Set.entries, Set.forEach, Set.size, Set.values, Set.keys, WeakMap.delete, WeakMap.get, WeakMap.set, WeakMap.has, WeakSet.delete, WeakSet.has, WeakSet.add, Atomics.load, Atomics.store, Atomics.add, Atomics.sub, Atomics.and, Atomics.or, Atomics.xor, Atomics.exchange, Atomics.compareExchange, Atomics.isLockFree, Atomics.wait, Atomics.waitAsync, Atomics.notify, JSON.parse, JSON.stringify, JSON.rawJSON, JSON.isRawJSON, Promise.all, Promise.allSettled, Promise.any, Promise.race, Promise.resolve, Promise.reject, Promise.then, Promise.catch, Promise.finally, Reflect.defineProperty, Reflect.deleteProperty, Reflect.apply, Reflect.construct, Reflect.get, Reflect.getOwnPropertyDescriptor, Reflect.getPrototypeOf, Reflect.has, Reflect.isExtensible, Reflect.ownKeys, Reflect.preventExtensions, Reflect.set, Reflect.setPrototypeOf, Proxy.revocable, Intl.getCanonicalLocales, Intl.supportedValuesOf, Intl.DateTimeFormat, Intl.NumberFormat, Intl.Collator, Intl.v8BreakIterator, Intl.PluralRules, Intl.RelativeTimeFormat, Intl.ListFormat, Intl.Locale, Intl.DisplayNames, Intl.Segmenter, WebAssembly.compile, WebAssembly.validate, WebAssembly.instantiate, WebAssembly.compileStreaming, WebAssembly.instantiateStreaming, WebAssembly.Module, WebAssembly.Instance, WebAssembly.Table, WebAssembly.Memory, WebAssembly.Global, WebAssembly.Tag, WebAssembly.Exception, WebAssembly.CompileError, WebAssembly.LinkError, WebAssembly.RuntimeError, Document.implementation, Document.URL, Document.documentURI, Document.compatMode, Document.characterSet, Document.charset, Document.inputEncoding, Document.contentType, Document.doctype, Document.documentElement, Document.xmlEncoding, Document.xmlVersion, Document.xmlStandalone, Document.domain, Document.referrer, Document.cookie, Document.lastModified, Document.readyState, Document.title, Document.dir, Document.body, Document.head, Document.images, Document.embeds, Document.plugins, Document.links, Document.forms, Document.scripts, Document.currentScript, Document.defaultView, Document.designMode, Document.onreadystatechange, Document.anchors, Document.applets, Document.fgColor, Document.linkColor, Document.vlinkColor, Document.alinkColor, Document.bgColor, Document.all, Document.scrollingElement, Document.onpointerlockchange, Document.onpointerlockerror, Document.hidden, Document.visibilityState, Document.wasDiscarded, Document.prerendering, Document.featurePolicy, Document.webkitVisibilityState, Document.webkitHidden, Document.onbeforecopy, Document.onbeforecut, Document.onbeforepaste, Document.onfreeze, Document.onprerenderingchange, Document.onresume, Document.onsearch, Document.onvisibilitychange, Document.fullscreenEnabled, Document.fullscreen, Document.onfullscreenchange, Document.onfullscreenerror, Document.webkitIsFullScreen, Document.webkitCurrentFullScreenElement, Document.webkitFullscreenEnabled, Document.webkitFullscreenElement, Document.onwebkitfullscreenchange, Document.onwebkitfullscreenerror, Document.rootElement, Document.pictureInPictureEnabled, Document.pictureInPictureElement, Document.onbeforexrselect, Document.onabort, Document.onbeforeinput, Document.onblur, Document.oncancel, Document.oncanplay, Document.oncanplaythrough, Document.onchange, Document.onclick, Document.onclose, Document.oncontextlost, Document.oncontextmenu, Document.oncontextrestored, Document.oncuechange, Document.ondblclick, Document.ondrag, Document.ondragend, Document.ondragenter, Document.ondragleave, Document.ondragover, Document.ondragstart, Document.ondrop, Document.ondurationchange, Document.onemptied, Document.onended, Document.onerror, Document.onfocus, Document.onformdata, Document.oninput, Document.oninvalid, Document.onkeydown, Document.onkeypress, Document.onkeyup, Document.onload, Document.onloadeddata, Document.onloadedmetadata, Document.onloadstart, Document.onmousedown, Document.onmouseenter, Document.onmouseleave, Document.onmousemove, Document.onmouseout, Document.onmouseover, Document.onmouseup, Document.onmousewheel, Document.onpause, Document.onplay, Document.onplaying, Document.onprogress, Document.onratechange, Document.onreset, Document.onresize, Document.onscroll, Document.onsecuritypolicyviolation, Document.onseeked, Document.onseeking, Document.onselect, Document.onslotchange, Document.onstalled, Document.onsubmit, Document.onsuspend, Document.ontimeupdate, Document.ontoggle, Document.onvolumechange, Document.onwaiting, Document.onwebkitanimationend, Document.onwebkitanimationiteration, Document.onwebkitanimationstart, Document.onwebkittransitionend, Document.onwheel, Document.onauxclick, Document.ongotpointercapture, Document.onlostpointercapture, Document.onpointerdown, Document.onpointermove, Document.onpointerrawupdate, Document.onpointerup, Document.onpointercancel, Document.onpointerover, Document.onpointerout, Document.onpointerenter, Document.onpointerleave, Document.onselectstart, Document.onselectionchange, Document.onanimationend, Document.onanimationiteration, Document.onanimationstart, Document.ontransitionrun, Document.ontransitionstart, Document.ontransitionend, Document.ontransitioncancel, Document.oncopy, Document.oncut, Document.onpaste, Document.children, Document.firstElementChild, Document.lastElementChild, Document.childElementCount, Document.activeElement, Document.styleSheets, Document.pointerLockElement, Document.fullscreenElement, Document.adoptedStyleSheets, Document.fonts, Document.adoptNode, Document.append, Document.captureEvents, Document.caretRangeFromPoint, Document.clear, Document.close, Document.createAttribute, Document.createAttributeNS, Document.createCDATASection, Document.createComment, Document.createDocumentFragment, Document.createElement, Document.createElementNS, Document.createEvent, Document.createExpression, Document.createNSResolver, Document.createNodeIterator, Document.createProcessingInstruction, Document.createRange, Document.createTextNode, Document.createTreeWalker, Document.elementFromPoint, Document.elementsFromPoint, Document.evaluate, Document.execCommand, Document.exitFullscreen, Document.exitPictureInPicture, Document.exitPointerLock, Document.getElementById, Document.getElementsByClassName, Document.getElementsByName, Document.getElementsByTagName, Document.getElementsByTagNameNS, Document.getSelection, Document.hasFocus, Document.importNode, Document.open, Document.prepend, Document.queryCommandEnabled, Document.queryCommandIndeterm, Document.queryCommandState, Document.queryCommandSupported, Document.queryCommandValue, Document.querySelector, Document.querySelectorAll, Document.releaseEvents, Document.replaceChildren, Document.webkitCancelFullScreen, Document.webkitExitFullscreen, Document.write, Document.writeln, Document.fragmentDirective, Document.onbeforematch, Document.onbeforetoggle, Document.timeline, Document.oncontentvisibilityautostatechange, Document.onscrollend, Document.getAnimations, Document.startViewTransition, Element.namespaceURI, Element.prefix, Element.localName, Element.tagName, Element.id, Element.className, Element.classList, Element.slot, Element.attributes, Element.shadowRoot, Element.part, Element.assignedSlot, Element.innerHTML, Element.outerHTML, Element.scrollTop, Element.scrollLeft, Element.scrollWidth, Element.scrollHeight, Element.clientTop, Element.clientLeft, Element.clientWidth, Element.clientHeight, Element.onbeforecopy, Element.onbeforecut, Element.onbeforepaste, Element.onsearch, Element.elementTiming, Element.onfullscreenchange, Element.onfullscreenerror, Element.onwebkitfullscreenchange, Element.onwebkitfullscreenerror, Element.role, Element.ariaAtomic, Element.ariaAutoComplete, Element.ariaBusy, Element.ariaBrailleLabel, Element.ariaBrailleRoleDescription, Element.ariaChecked, Element.ariaColCount, Element.ariaColIndex, Element.ariaColSpan, Element.ariaCurrent, Element.ariaDescription, Element.ariaDisabled, Element.ariaExpanded, Element.ariaHasPopup, Element.ariaHidden, Element.ariaInvalid, Element.ariaKeyShortcuts, Element.ariaLabel, Element.ariaLevel, Element.ariaLive, Element.ariaModal, Element.ariaMultiLine, Element.ariaMultiSelectable, Element.ariaOrientation, Element.ariaPlaceholder, Element.ariaPosInSet, Element.ariaPressed, Element.ariaReadOnly, Element.ariaRelevant, Element.ariaRequired, Element.ariaRoleDescription, Element.ariaRowCount, Element.ariaRowIndex, Element.ariaRowSpan, Element.ariaSelected, Element.ariaSetSize, Element.ariaSort, Element.ariaValueMax, Element.ariaValueMin, Element.ariaValueNow, Element.ariaValueText, Element.children, Element.firstElementChild, Element.lastElementChild, Element.childElementCount, Element.previousElementSibling, Element.nextElementSibling, Element.after, Element.animate, Element.append, Element.attachShadow, Element.before, Element.closest, Element.computedStyleMap, Element.getAttribute, Element.getAttributeNS, Element.getAttributeNames, Element.getAttributeNode, Element.getAttributeNodeNS, Element.getBoundingClientRect, Element.getClientRects, Element.getElementsByClassName, Element.getElementsByTagName, Element.getElementsByTagNameNS, Element.getInnerHTML, Element.hasAttribute, Element.hasAttributeNS, Element.hasAttributes, Element.hasPointerCapture, Element.insertAdjacentElement, Element.insertAdjacentHTML, Element.insertAdjacentText, Element.matches, Element.prepend, Element.querySelector, Element.querySelectorAll, Element.releasePointerCapture, Element.remove, Element.removeAttribute, Element.removeAttributeNS, Element.removeAttributeNode, Element.replaceChildren, Element.replaceWith, Element.requestFullscreen, Element.requestPointerLock, Element.scroll, Element.scrollBy, Element.scrollIntoView, Element.scrollIntoViewIfNeeded, Element.scrollTo, Element.setAttribute, Element.setAttributeNS, Element.setAttributeNode, Element.setAttributeNodeNS, Element.setPointerCapture, Element.toggleAttribute, Element.webkitMatchesSelector, Element.webkitRequestFullScreen, Element.webkitRequestFullscreen, Element.checkVisibility, Element.getAnimations, Element.setHTML', - }, - 'Firefox': { - version: 112, - windowKeys: `undefined, globalThis, Array, Boolean, JSON, Date, Math, Number, String, RegExp, Error, InternalError, AggregateError, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, URIError, ArrayBuffer, Int8Array, Uint8Array, Int16Array, Uint16Array, Int32Array, Uint32Array, Float32Array, Float64Array, Uint8ClampedArray, BigInt64Array, BigUint64Array, BigInt, Proxy, WeakMap, Set, DataView, Symbol, Intl, Reflect, WeakSet, Atomics, WebAssembly, FinalizationRegistry, WeakRef, NaN, Infinity, isNaN, isFinite, parseFloat, parseInt, escape, unescape, decodeURI, encodeURI, decodeURIComponent, encodeURIComponent, CryptoKey, FocusEvent, CSSRuleList, MediaStreamTrackAudioSourceNode, SVGGeometryElement, SVGElement, SVGPatternElement, WebSocket, HTMLAllCollection, UIEvent, PageTransitionEvent, AuthenticatorAssertionResponse, ScreenOrientation, MediaCapabilitiesInfo, SVGImageElement, NodeIterator, SVGFEOffsetElement, AnimationPlaybackEvent, MessageChannel, TextDecoderStream, ShadowRoot, SVGAnimatedNumberList, SVGEllipseElement, DOMStringMap, AudioWorkletNode, TextMetrics, SVGPointList, SVGSymbolElement, DocumentType, StorageEvent, SVGAnimatedLength, PerformanceObserver, WebGLSampler, PushSubscription, CustomElementRegistry, SVGUnitTypes, SVGFEMorphologyElement, NodeFilter, File, Geolocation, ProcessingInstruction, AudioScheduledSourceNode, FileReader, IDBObjectStore, SVGStringList, HTMLParagraphElement, IDBDatabase, SVGLinearGradientElement, Animation, HTMLIFrameElement, HTMLTableSectionElement, Worker, TimeRanges, Navigator, InputEvent, GamepadHapticActuator, SVGMatrix, Worklet, SVGFEMergeNodeElement, DOMTokenList, MediaQueryListEvent, HTMLParamElement, MessagePort, SVGLengthList, ResizeObserverSize, TextEncoderStream, PromiseRejectionEvent, SVGTransformList, DOMPoint, SVGTextElement, WebGL2RenderingContext, PluginArray, IDBVersionChangeEvent, FontFaceSetLoadEvent, CSSStyleDeclaration, SVGGraphicsElement, HTMLOListElement, HTMLTextAreaElement, Storage, XPathEvaluator, MouseScrollEvent, HTMLCanvasElement, HTMLBodyElement, HTMLCollection, HTMLHtmlElement, MediaList, HTMLAudioElement, IDBFactory, SVGAnimatedTransformList, MimeType, SVGAnimatedPreserveAspectRatio, HTMLFrameElement, HTMLLegendElement, HTMLMapElement, SVGAnimateMotionElement, HTMLFrameSetElement, CSSGroupingRule, Clipboard, IIRFilterNode, ReadableStreamDefaultController, FileSystemFileHandle, HTMLElement, CSSConditionRule, CSS, SVGFEComponentTransferElement, DOMRectList, AudioWorklet, SourceBuffer, HTMLStyleElement, DocumentTimeline, IDBKeyRange, DOMRequest, PerformanceTiming, GeolocationPosition, SVGTextPositioningElement, OfflineResourceList, IDBOpenDBRequest, SVGFEFuncGElement, MouseEvent, FontFaceSet, OffscreenCanvasRenderingContext2D, OscillatorNode, SpeechSynthesisUtterance, AudioContext, FileSystemEntry, PaintRequest, SVGFEConvolveMatrixElement, ChannelSplitterNode, AudioBufferSourceNode, CaretPosition, AbortSignal, HTMLBRElement, XSLTProcessor, SVGFESpecularLightingElement, mozRTCPeerConnection, XMLSerializer, StorageManager, HTMLImageElement, WebGLQuery, FileSystemHandle, Permissions, BaseAudioContext, MediaStream, History, DOMStringList, StereoPannerNode, ReadableStreamDefaultReader, HTMLDListElement, MutationEvent, SVGComponentTransferFunctionElement, Notification, DynamicsCompressorNode, LockManager, Option, HTMLMeterElement, RTCPeerConnection, CloseEvent, AudioBuffer, ByteLengthQueuingStrategy, SVGFECompositeElement, HTMLTrackElement, ServiceWorkerContainer, MediaStreamTrack, WebGLContextEvent, WritableStreamDefaultController, SVGPathElement, BarProp, PerformanceObserverEntryList, RTCRtpReceiver, StyleSheet, WebGLUniformLocation, HTMLMetaElement, CanvasPattern, OffscreenCanvas, SVGTransform, SVGTextContentElement, PerformanceServerTiming, TrackEvent, XPathExpression, AnimationTimeline, MediaError, HTMLAnchorElement, XMLHttpRequest, SecurityPolicyViolationEvent, CSSMozDocumentRule, CSSImportRule, SVGLength, WaveShaperNode, RTCTrackEvent, RTCRtpSender, CSSAnimation, CSSFontPaletteValuesRule, AnimationEffect, CSSContainerRule, RTCStatsReport, SourceBufferList, HTMLHeadingElement, CanvasCaptureMediaStream, HTMLOptionElement, SVGSVGElement, WebGLActiveInfo, MIDIPort, HTMLUListElement, XPathResult, SVGUseElement, Credential, PublicKeyCredential, DOMQuad, Selection, HTMLDataElement, CSSLayerStatementRule, WebGLShader, Location, MIDIAccess, MediaRecorderErrorEvent, SVGFEGaussianBlurElement, MediaStreamEvent, CSSFontFeatureValuesRule, AbortController, RTCIceCandidate, HTMLLabelElement, PerformanceMeasure, HTMLDirectoryElement, SVGStopElement, PermissionStatus, PerformancePaintTiming, FileSystemFileEntry, SVGMarkerElement, console, SharedWorker, WebGLVertexArrayObject, HTMLOptionsCollection, HTMLTitleElement, TreeWalker, CompositionEvent, IDBCursor, TransformStream, PerformanceNavigation, Blob, SpeechSynthesisErrorEvent, CSSStyleSheet, HTMLUnknownElement, KeyEvent, HTMLOptGroupElement, CanvasGradient, AnalyserNode, Element, AnimationEvent, HTMLFieldSetElement, MediaSession, MutationObserver, SVGAnimateTransformElement, OfflineAudioContext, CacheStorage, MediaKeyStatusMap, GamepadEvent, RTCPeerConnectionIceEvent, AudioParam, AbstractRange, TextEncoder, FileSystemDirectoryHandle, CSSSupportsRule, SVGPreserveAspectRatio, PerformanceEntry, mozRTCSessionDescription, VideoPlaybackQuality, HTMLTableRowElement, HTMLFontElement, MediaKeys, DataTransfer, CSSPageRule, SVGAngle, WebGLFramebuffer, WebGLRenderbuffer, Directory, HTMLAreaElement, MimeTypeArray, NamedNodeMap, CSSKeyframeRule, XMLDocument, HTMLSlotElement, MediaDevices, IDBTransaction, HTMLModElement, MediaKeyError, SVGFEFloodElement, DOMParser, HTMLScriptElement, ReadableStreamBYOBReader, HTMLDataListElement, MediaElementAudioSourceNode, PeriodicWave, DragEvent, SVGStyleElement, SubtleCrypto, TransformStreamDefaultController, MessageEvent, WebGLProgram, SVGSetElement, WebGLShaderPrecisionFormat, ErrorEvent, NodeList, GamepadButton, MediaDeviceInfo, HTMLMediaElement, DeviceMotionEvent, ImageData, Range, PushSubscriptionOptions, OfflineAudioCompletionEvent, DOMPointReadOnly, DocumentFragment, Attr, BroadcastChannel, HTMLLinkElement, DOMMatrixReadOnly, AuthenticatorAttestationResponse, IdleDeadline, ImageBitmapRenderingContext, MediaKeySystemAccess, SVGPoint, SVGFEDropShadowElement, SVGFEDiffuseLightingElement, SVGRadialGradientElement, RTCDataChannelEvent, Headers, FileSystemDirectoryReader, SVGFEDisplacementMapElement, SVGDefsElement, SVGFEDistantLightElement, HTMLFormControlsCollection, WebGLRenderingContext, HTMLTableElement, SVGNumber, SVGAnimatedInteger, VisualViewport, SpeechSynthesisVoice, WheelEvent, SVGAnimatedNumber, GamepadPose, ResizeObserver, TextDecoder, FormDataEvent, FileSystem, IntersectionObserverEntry, SVGPolylineElement, Text, CanvasRenderingContext2D, CSSFontFaceRule, SVGGradientElement, WritableStream, SVGNumberList, SpeechSynthesisEvent, WritableStreamDefaultWriter, SVGFilterElement, URL, SVGRect, SVGFEImageElement, AudioDestinationNode, IDBRequest, MathMLElement, HTMLTimeElement, TextTrackCue, RadioNodeList, SVGTSpanElement, SVGAnimatedLengthList, SVGAnimatedString, HTMLSourceElement, Lock, ProgressEvent, PopupBlockedEvent, ValidityState, WebKitCSSMatrix, AudioListener, HTMLFormElement, PerformanceEventTiming, HTMLProgressElement, Gamepad, MediaKeySession, MediaStreamAudioSourceNode, CSSRule, HTMLPreElement, webkitURL, AuthenticatorResponse, HTMLEmbedElement, HTMLDivElement, MediaStreamAudioDestinationNode, CredentialsContainer, SVGScriptElement, MutationRecord, ConvolverNode, SVGFEPointLightElement, Screen, ClipboardEvent, SVGFETurbulenceElement, SVGFEBlendElement, GeolocationCoordinates, TextTrackList, FontFace, HTMLInputElement, Request, SVGGElement, SVGClipPathElement, TimeEvent, TextTrackCueList, BiquadFilterNode, SVGTitleElement, SVGFETileElement, SVGFEFuncRElement, HTMLButtonElement, Path2D, HTMLTableCellElement, StaticRange, SVGLineElement, CSSCounterStyleRule, HTMLQuoteElement, AudioParamMap, DeviceOrientationEvent, IDBCursorWithValue, MediaKeyMessageEvent, SVGAnimatedEnumeration, MIDIOutput, HTMLHRElement, ImageBitmap, CSSStyleRule, MIDIOutputMap, SVGAElement, ElementInternals, VTTCue, MediaMetadata, PannerNode, SVGForeignObjectElement, SVGSwitchElement, HTMLVideoElement, MediaEncryptedEvent, EventSource, SVGAnimateElement, DOMException, CSSTransition, Image, VTTRegion, CharacterData, SVGFEFuncAElement, SVGDescElement, SubmitEvent, RTCSessionDescription, SVGAnimatedBoolean, StyleSheetList, HTMLTableColElement, HTMLMarqueeElement, CSSKeyframesRule, SVGMetadataElement, MIDIInputMap, PushManager, GainNode, DOMRect, SVGMaskElement, HTMLMenuElement, RTCDTMFToneChangeEvent, IDBIndex, CSSMediaRule, HashChangeEvent, ServiceWorker, SVGFEFuncBElement, BlobEvent, DOMImplementation, GeolocationPositionError, SVGMPathElement, SVGFEColorMatrixElement, KeyboardEvent, HTMLLIElement, RTCCertificate, SpeechSynthesis, ReadableStreamBYOBRequest, DelayNode, XMLHttpRequestEventTarget, PopStateEvent, Cache, ScrollAreaEvent, RTCDtlsTransport, SVGTextPathElement, XMLHttpRequestUpload, HTMLOutputElement, MediaRecorder, PaintRequestList, SVGRectElement, AudioNode, DOMMatrix, MediaSource, FormData, NavigationPreloadManager, HTMLTableCaptionElement, CustomEvent, MediaCapabilities, SVGFEMergeElement, MediaStreamTrackEvent, Audio, CSS2Properties, FileList, SVGAnimatedRect, FileSystemWritableFileStream, ReadableStream, HTMLPictureElement, HTMLSelectElement, AudioProcessingEvent, PerformanceResourceTiming, Plugin, Crypto, CSSLayerBlockRule, ConstantSourceNode, HTMLSpanElement, DataTransferItem, ServiceWorkerRegistration, WebGLTransformFeedback, SVGViewElement, CSSNamespaceRule, URLSearchParams, WebGLBuffer, MediaQueryList, PointerEvent, SVGPolygonElement, KeyframeEffect, RTCDTMFSender, ResizeObserverEntry, SVGCircleElement, SVGAnimationElement, WebGLTexture, DOMRectReadOnly, WebGLSync, IntersectionObserver, MIDIMessageEvent, ReadableByteStreamController, HTMLBaseElement, CountQueuingStrategy, mozRTCIceCandidate, DataTransferItemList, HTMLHeadElement, CDATASection, HTMLDialogElement, HTMLTemplateElement, RTCDataChannel, PerformanceMark, SVGFESpotLightElement, BeforeUnloadEvent, MIDIConnectionEvent, RTCRtpTransceiver, TextTrack, TransitionEvent, HTMLDetailsElement, Comment, HTMLObjectElement, ChannelMergerNode, SVGAnimatedAngle, Response, FileSystemDirectoryEntry, MIDIInput, ScriptProcessorNode, Function, Object, eval, EventTarget, Window, close, stop, focus, blur, open, alert, confirm, prompt, print, postMessage, captureEvents, releaseEvents, getSelection, getComputedStyle, matchMedia, moveTo, moveBy, resizeTo, resizeBy, scroll, scrollTo, scrollBy, getDefaultComputedStyle, scrollByLines, scrollByPages, sizeToContent, updateCommands, find, dump, setResizable, requestIdleCallback, cancelIdleCallback, requestAnimationFrame, cancelAnimationFrame, reportError, btoa, atob, setTimeout, clearTimeout, setInterval, clearInterval, queueMicrotask, createImageBitmap, structuredClone, fetch, self, name, history, customElements, locationbar, menubar, personalbar, scrollbars, statusbar, toolbar, status, closed, event, frames, length, opener, parent, frameElement, navigator, clientInformation, external, applicationCache, screen, innerWidth, innerHeight, scrollX, pageXOffset, scrollY, pageYOffset, screenLeft, screenTop, screenX, screenY, outerWidth, outerHeight, performance, mozInnerScreenX, mozInnerScreenY, devicePixelRatio, scrollMaxX, scrollMaxY, fullScreen, ondevicemotion, ondeviceorientation, ondeviceorientationabsolute, InstallTrigger, visualViewport, crypto, onabort, onblur, onfocus, onauxclick, onbeforeinput, oncanplay, oncanplaythrough, onchange, onclick, onclose, oncontextmenu, oncopy, oncuechange, oncut, ondblclick, ondrag, ondragend, ondragenter, ondragexit, ondragleave, ondragover, ondragstart, ondrop, ondurationchange, onemptied, onended, onformdata, oninput, oninvalid, onkeydown, onkeypress, onkeyup, onload, onloadeddata, onloadedmetadata, onloadstart, onmousedown, onmouseenter, onmouseleave, onmousemove, onmouseout, onmouseover, onmouseup, onwheel, onpaste, onpause, onplay, onplaying, onprogress, onratechange, onreset, onresize, onscroll, onscrollend, onsecuritypolicyviolation, onseeked, onseeking, onselect, onslotchange, onstalled, onsubmit, onsuspend, ontimeupdate, onvolumechange, onwaiting, onselectstart, onselectionchange, ontoggle, onpointercancel, onpointerdown, onpointerup, onpointermove, onpointerout, onpointerover, onpointerenter, onpointerleave, ongotpointercapture, onlostpointercapture, onmozfullscreenchange, onmozfullscreenerror, onanimationcancel, onanimationend, onanimationiteration, onanimationstart, ontransitioncancel, ontransitionend, ontransitionrun, ontransitionstart, onwebkitanimationend, onwebkitanimationiteration, onwebkitanimationstart, onwebkittransitionend, onerror, speechSynthesis, onafterprint, onbeforeprint, onbeforeunload, onhashchange, onlanguagechange, onmessage, onmessageerror, onoffline, ononline, onpagehide, onpageshow, onpopstate, onrejectionhandled, onstorage, onunhandledrejection, onunload, ongamepadconnected, ongamepaddisconnected, localStorage, origin, crossOriginIsolated, isSecureContext, indexedDB, caches, sessionStorage, window, document, location, top, netscape, Node, Document, HTMLDocument, EventCounts, Map, Promise, Event`, - cssKeys: `alignContent, align-content, alignItems, align-items, alignSelf, align-self, aspectRatio, aspect-ratio, backfaceVisibility, backface-visibility, borderCollapse, border-collapse, borderImageRepeat, border-image-repeat, boxDecorationBreak, box-decoration-break, boxSizing, box-sizing, breakInside, break-inside, captionSide, caption-side, clear, colorInterpolation, color-interpolation, colorInterpolationFilters, color-interpolation-filters, columnCount, column-count, columnFill, column-fill, columnSpan, column-span, contain, containerType, container-type, direction, display, dominantBaseline, dominant-baseline, emptyCells, empty-cells, flexDirection, flex-direction, flexWrap, flex-wrap, cssFloat, float, fontKerning, font-kerning, fontLanguageOverride, font-language-override, fontOpticalSizing, font-optical-sizing, fontSizeAdjust, font-size-adjust, fontStretch, font-stretch, fontStyle, font-style, fontVariantCaps, font-variant-caps, fontVariantEastAsian, font-variant-east-asian, fontVariantLigatures, font-variant-ligatures, fontVariantNumeric, font-variant-numeric, fontVariantPosition, font-variant-position, fontWeight, font-weight, gridAutoFlow, grid-auto-flow, hyphens, imageOrientation, image-orientation, imageRendering, image-rendering, imeMode, ime-mode, isolation, justifyContent, justify-content, justifyItems, justify-items, justifySelf, justify-self, lineBreak, line-break, listStylePosition, list-style-position, maskType, mask-type, mixBlendMode, mix-blend-mode, MozBoxAlign, -moz-box-align, MozBoxDirection, -moz-box-direction, MozBoxOrient, -moz-box-orient, MozBoxPack, -moz-box-pack, MozFloatEdge, -moz-float-edge, MozOrient, -moz-orient, MozTextSizeAdjust, -moz-text-size-adjust, MozUserFocus, -moz-user-focus, MozUserInput, -moz-user-input, MozUserModify, -moz-user-modify, MozWindowDragging, -moz-window-dragging, objectFit, object-fit, offsetRotate, offset-rotate, outlineStyle, outline-style, overflowAnchor, overflow-anchor, overflowWrap, overflow-wrap, paintOrder, paint-order, pointerEvents, pointer-events, position, printColorAdjust, print-color-adjust, resize, rubyAlign, ruby-align, rubyPosition, ruby-position, scrollBehavior, scroll-behavior, scrollSnapAlign, scroll-snap-align, scrollSnapStop, scroll-snap-stop, scrollSnapType, scroll-snap-type, scrollbarGutter, scrollbar-gutter, scrollbarWidth, scrollbar-width, shapeRendering, shape-rendering, strokeLinecap, stroke-linecap, strokeLinejoin, stroke-linejoin, tableLayout, table-layout, textAlign, text-align, textAlignLast, text-align-last, textAnchor, text-anchor, textCombineUpright, text-combine-upright, textDecorationLine, text-decoration-line, textDecorationSkipInk, text-decoration-skip-ink, textDecorationStyle, text-decoration-style, textEmphasisPosition, text-emphasis-position, textJustify, text-justify, textOrientation, text-orientation, textRendering, text-rendering, textTransform, text-transform, textUnderlinePosition, text-underline-position, touchAction, touch-action, transformBox, transform-box, transformStyle, transform-style, unicodeBidi, unicode-bidi, userSelect, user-select, vectorEffect, vector-effect, visibility, webkitLineClamp, WebkitLineClamp, -webkit-line-clamp, whiteSpace, white-space, wordBreak, word-break, writingMode, writing-mode, zIndex, z-index, appearance, MozForceBrokenImageIcon, -moz-force-broken-image-icon, breakAfter, break-after, breakBefore, break-before, clipRule, clip-rule, fillRule, fill-rule, fillOpacity, fill-opacity, strokeOpacity, stroke-opacity, fontSynthesisSmallCaps, font-synthesis-small-caps, fontSynthesisStyle, font-synthesis-style, fontSynthesisWeight, font-synthesis-weight, MozBoxOrdinalGroup, -moz-box-ordinal-group, order, flexGrow, flex-grow, flexShrink, flex-shrink, MozBoxFlex, -moz-box-flex, strokeMiterlimit, stroke-miterlimit, overflowBlock, overflow-block, overflowInline, overflow-inline, overflowX, overflow-x, overflowY, overflow-y, overscrollBehaviorBlock, overscroll-behavior-block, overscrollBehaviorInline, overscroll-behavior-inline, overscrollBehaviorX, overscroll-behavior-x, overscrollBehaviorY, overscroll-behavior-y, floodOpacity, flood-opacity, opacity, shapeImageThreshold, shape-image-threshold, stopOpacity, stop-opacity, borderBlockEndStyle, border-block-end-style, borderBlockStartStyle, border-block-start-style, borderBottomStyle, border-bottom-style, borderInlineEndStyle, border-inline-end-style, borderInlineStartStyle, border-inline-start-style, borderLeftStyle, border-left-style, borderRightStyle, border-right-style, borderTopStyle, border-top-style, columnRuleStyle, column-rule-style, accentColor, accent-color, animationDelay, animation-delay, animationDirection, animation-direction, animationDuration, animation-duration, animationFillMode, animation-fill-mode, animationIterationCount, animation-iteration-count, animationName, animation-name, animationPlayState, animation-play-state, animationTimingFunction, animation-timing-function, backdropFilter, backdrop-filter, backgroundAttachment, background-attachment, backgroundBlendMode, background-blend-mode, backgroundClip, background-clip, backgroundImage, background-image, backgroundOrigin, background-origin, backgroundPositionX, background-position-x, backgroundPositionY, background-position-y, backgroundRepeat, background-repeat, backgroundSize, background-size, borderImageOutset, border-image-outset, borderImageSlice, border-image-slice, borderImageWidth, border-image-width, borderSpacing, border-spacing, boxShadow, box-shadow, caretColor, caret-color, clip, clipPath, clip-path, color, colorScheme, color-scheme, columnWidth, column-width, containerName, container-name, content, counterIncrement, counter-increment, counterReset, counter-reset, counterSet, counter-set, cursor, d, filter, flexBasis, flex-basis, fontFamily, font-family, fontFeatureSettings, font-feature-settings, fontPalette, font-palette, fontSize, font-size, fontVariantAlternates, font-variant-alternates, fontVariationSettings, font-variation-settings, gridTemplateAreas, grid-template-areas, hyphenateCharacter, hyphenate-character, letterSpacing, letter-spacing, lineHeight, line-height, listStyleType, list-style-type, maskClip, mask-clip, maskComposite, mask-composite, maskImage, mask-image, maskMode, mask-mode, maskOrigin, mask-origin, maskPositionX, mask-position-x, maskPositionY, mask-position-y, maskRepeat, mask-repeat, maskSize, mask-size, offsetPath, offset-path, page, perspective, quotes, rotate, scale, scrollbarColor, scrollbar-color, shapeOutside, shape-outside, strokeDasharray, stroke-dasharray, strokeDashoffset, stroke-dashoffset, strokeWidth, stroke-width, tabSize, tab-size, textDecorationThickness, text-decoration-thickness, textEmphasisStyle, text-emphasis-style, textOverflow, text-overflow, textShadow, text-shadow, transitionDelay, transition-delay, transitionDuration, transition-duration, transitionProperty, transition-property, transitionTimingFunction, transition-timing-function, translate, verticalAlign, vertical-align, willChange, will-change, wordSpacing, word-spacing, objectPosition, object-position, perspectiveOrigin, perspective-origin, offsetAnchor, offset-anchor, fill, stroke, transformOrigin, transform-origin, gridTemplateColumns, grid-template-columns, gridTemplateRows, grid-template-rows, borderImageSource, border-image-source, listStyleImage, list-style-image, gridAutoColumns, grid-auto-columns, gridAutoRows, grid-auto-rows, transform, columnGap, column-gap, rowGap, row-gap, markerEnd, marker-end, markerMid, marker-mid, markerStart, marker-start, containIntrinsicBlockSize, contain-intrinsic-block-size, containIntrinsicHeight, contain-intrinsic-height, containIntrinsicInlineSize, contain-intrinsic-inline-size, containIntrinsicWidth, contain-intrinsic-width, gridColumnEnd, grid-column-end, gridColumnStart, grid-column-start, gridRowEnd, grid-row-end, gridRowStart, grid-row-start, maxBlockSize, max-block-size, maxHeight, max-height, maxInlineSize, max-inline-size, maxWidth, max-width, cx, cy, offsetDistance, offset-distance, textIndent, text-indent, x, y, borderBottomLeftRadius, border-bottom-left-radius, borderBottomRightRadius, border-bottom-right-radius, borderEndEndRadius, border-end-end-radius, borderEndStartRadius, border-end-start-radius, borderStartEndRadius, border-start-end-radius, borderStartStartRadius, border-start-start-radius, borderTopLeftRadius, border-top-left-radius, borderTopRightRadius, border-top-right-radius, blockSize, block-size, height, inlineSize, inline-size, minBlockSize, min-block-size, minHeight, min-height, minInlineSize, min-inline-size, minWidth, min-width, width, paddingBlockEnd, padding-block-end, paddingBlockStart, padding-block-start, paddingBottom, padding-bottom, paddingInlineEnd, padding-inline-end, paddingInlineStart, padding-inline-start, paddingLeft, padding-left, paddingRight, padding-right, paddingTop, padding-top, r, shapeMargin, shape-margin, rx, ry, scrollPaddingBlockEnd, scroll-padding-block-end, scrollPaddingBlockStart, scroll-padding-block-start, scrollPaddingBottom, scroll-padding-bottom, scrollPaddingInlineEnd, scroll-padding-inline-end, scrollPaddingInlineStart, scroll-padding-inline-start, scrollPaddingLeft, scroll-padding-left, scrollPaddingRight, scroll-padding-right, scrollPaddingTop, scroll-padding-top, borderBlockEndWidth, border-block-end-width, borderBlockStartWidth, border-block-start-width, borderBottomWidth, border-bottom-width, borderInlineEndWidth, border-inline-end-width, borderInlineStartWidth, border-inline-start-width, borderLeftWidth, border-left-width, borderRightWidth, border-right-width, borderTopWidth, border-top-width, columnRuleWidth, column-rule-width, outlineWidth, outline-width, webkitTextStrokeWidth, WebkitTextStrokeWidth, -webkit-text-stroke-width, outlineOffset, outline-offset, overflowClipMargin, overflow-clip-margin, scrollMarginBlockEnd, scroll-margin-block-end, scrollMarginBlockStart, scroll-margin-block-start, scrollMarginBottom, scroll-margin-bottom, scrollMarginInlineEnd, scroll-margin-inline-end, scrollMarginInlineStart, scroll-margin-inline-start, scrollMarginLeft, scroll-margin-left, scrollMarginRight, scroll-margin-right, scrollMarginTop, scroll-margin-top, bottom, insetBlockEnd, inset-block-end, insetBlockStart, inset-block-start, insetInlineEnd, inset-inline-end, insetInlineStart, inset-inline-start, left, marginBlockEnd, margin-block-end, marginBlockStart, margin-block-start, marginBottom, margin-bottom, marginInlineEnd, margin-inline-end, marginInlineStart, margin-inline-start, marginLeft, margin-left, marginRight, margin-right, marginTop, margin-top, right, textUnderlineOffset, text-underline-offset, top, backgroundColor, background-color, borderBlockEndColor, border-block-end-color, borderBlockStartColor, border-block-start-color, borderBottomColor, border-bottom-color, borderInlineEndColor, border-inline-end-color, borderInlineStartColor, border-inline-start-color, borderLeftColor, border-left-color, borderRightColor, border-right-color, borderTopColor, border-top-color, columnRuleColor, column-rule-color, floodColor, flood-color, lightingColor, lighting-color, outlineColor, outline-color, stopColor, stop-color, textDecorationColor, text-decoration-color, textEmphasisColor, text-emphasis-color, webkitTextFillColor, WebkitTextFillColor, -webkit-text-fill-color, webkitTextStrokeColor, WebkitTextStrokeColor, -webkit-text-stroke-color, background, backgroundPosition, background-position, borderColor, border-color, borderStyle, border-style, borderWidth, border-width, borderTop, border-top, borderRight, border-right, borderBottom, border-bottom, borderLeft, border-left, borderBlockStart, border-block-start, borderBlockEnd, border-block-end, borderInlineStart, border-inline-start, borderInlineEnd, border-inline-end, border, borderRadius, border-radius, borderImage, border-image, borderBlockWidth, border-block-width, borderBlockStyle, border-block-style, borderBlockColor, border-block-color, borderInlineWidth, border-inline-width, borderInlineStyle, border-inline-style, borderInlineColor, border-inline-color, borderBlock, border-block, borderInline, border-inline, overflow, overscrollBehavior, overscroll-behavior, container, pageBreakBefore, page-break-before, pageBreakAfter, page-break-after, pageBreakInside, page-break-inside, offset, columns, columnRule, column-rule, font, fontVariant, font-variant, fontSynthesis, font-synthesis, marker, textEmphasis, text-emphasis, webkitTextStroke, WebkitTextStroke, -webkit-text-stroke, listStyle, list-style, margin, marginBlock, margin-block, marginInline, margin-inline, scrollMargin, scroll-margin, scrollMarginBlock, scroll-margin-block, scrollMarginInline, scroll-margin-inline, outline, padding, paddingBlock, padding-block, paddingInline, padding-inline, scrollPadding, scroll-padding, scrollPaddingBlock, scroll-padding-block, scrollPaddingInline, scroll-padding-inline, flexFlow, flex-flow, flex, gap, gridRow, grid-row, gridColumn, grid-column, gridArea, grid-area, gridTemplate, grid-template, grid, placeContent, place-content, placeSelf, place-self, placeItems, place-items, inset, insetBlock, inset-block, insetInline, inset-inline, containIntrinsicSize, contain-intrinsic-size, mask, maskPosition, mask-position, textDecoration, text-decoration, transition, animation, all, webkitBackgroundClip, WebkitBackgroundClip, -webkit-background-clip, webkitBackgroundOrigin, WebkitBackgroundOrigin, -webkit-background-origin, webkitBackgroundSize, WebkitBackgroundSize, -webkit-background-size, MozBorderStartColor, -moz-border-start-color, MozBorderStartStyle, -moz-border-start-style, MozBorderStartWidth, -moz-border-start-width, MozBorderEndColor, -moz-border-end-color, MozBorderEndStyle, -moz-border-end-style, MozBorderEndWidth, -moz-border-end-width, webkitBorderTopLeftRadius, WebkitBorderTopLeftRadius, -webkit-border-top-left-radius, webkitBorderTopRightRadius, WebkitBorderTopRightRadius, -webkit-border-top-right-radius, webkitBorderBottomRightRadius, WebkitBorderBottomRightRadius, -webkit-border-bottom-right-radius, webkitBorderBottomLeftRadius, WebkitBorderBottomLeftRadius, -webkit-border-bottom-left-radius, MozTransform, -moz-transform, webkitTransform, WebkitTransform, -webkit-transform, MozPerspective, -moz-perspective, webkitPerspective, WebkitPerspective, -webkit-perspective, MozPerspectiveOrigin, -moz-perspective-origin, webkitPerspectiveOrigin, WebkitPerspectiveOrigin, -webkit-perspective-origin, MozBackfaceVisibility, -moz-backface-visibility, webkitBackfaceVisibility, WebkitBackfaceVisibility, -webkit-backface-visibility, MozTransformStyle, -moz-transform-style, webkitTransformStyle, WebkitTransformStyle, -webkit-transform-style, MozTransformOrigin, -moz-transform-origin, webkitTransformOrigin, WebkitTransformOrigin, -webkit-transform-origin, MozAppearance, -moz-appearance, webkitAppearance, WebkitAppearance, -webkit-appearance, webkitBoxShadow, WebkitBoxShadow, -webkit-box-shadow, webkitFilter, WebkitFilter, -webkit-filter, MozFontFeatureSettings, -moz-font-feature-settings, MozFontLanguageOverride, -moz-font-language-override, colorAdjust, color-adjust, MozHyphens, -moz-hyphens, webkitTextSizeAdjust, WebkitTextSizeAdjust, -webkit-text-size-adjust, wordWrap, word-wrap, MozTabSize, -moz-tab-size, MozMarginStart, -moz-margin-start, MozMarginEnd, -moz-margin-end, MozPaddingStart, -moz-padding-start, MozPaddingEnd, -moz-padding-end, webkitFlexDirection, WebkitFlexDirection, -webkit-flex-direction, webkitFlexWrap, WebkitFlexWrap, -webkit-flex-wrap, webkitJustifyContent, WebkitJustifyContent, -webkit-justify-content, webkitAlignContent, WebkitAlignContent, -webkit-align-content, webkitAlignItems, WebkitAlignItems, -webkit-align-items, webkitFlexGrow, WebkitFlexGrow, -webkit-flex-grow, webkitFlexShrink, WebkitFlexShrink, -webkit-flex-shrink, webkitAlignSelf, WebkitAlignSelf, -webkit-align-self, webkitOrder, WebkitOrder, -webkit-order, webkitFlexBasis, WebkitFlexBasis, -webkit-flex-basis, MozBoxSizing, -moz-box-sizing, webkitBoxSizing, WebkitBoxSizing, -webkit-box-sizing, gridColumnGap, grid-column-gap, gridRowGap, grid-row-gap, webkitClipPath, WebkitClipPath, -webkit-clip-path, webkitMaskRepeat, WebkitMaskRepeat, -webkit-mask-repeat, webkitMaskPositionX, WebkitMaskPositionX, -webkit-mask-position-x, webkitMaskPositionY, WebkitMaskPositionY, -webkit-mask-position-y, webkitMaskClip, WebkitMaskClip, -webkit-mask-clip, webkitMaskOrigin, WebkitMaskOrigin, -webkit-mask-origin, webkitMaskSize, WebkitMaskSize, -webkit-mask-size, webkitMaskComposite, WebkitMaskComposite, -webkit-mask-composite, webkitMaskImage, WebkitMaskImage, -webkit-mask-image, MozUserSelect, -moz-user-select, webkitUserSelect, WebkitUserSelect, -webkit-user-select, MozTransitionDuration, -moz-transition-duration, webkitTransitionDuration, WebkitTransitionDuration, -webkit-transition-duration, MozTransitionTimingFunction, -moz-transition-timing-function, webkitTransitionTimingFunction, WebkitTransitionTimingFunction, -webkit-transition-timing-function, MozTransitionProperty, -moz-transition-property, webkitTransitionProperty, WebkitTransitionProperty, -webkit-transition-property, MozTransitionDelay, -moz-transition-delay, webkitTransitionDelay, WebkitTransitionDelay, -webkit-transition-delay, MozAnimationName, -moz-animation-name, webkitAnimationName, WebkitAnimationName, -webkit-animation-name, MozAnimationDuration, -moz-animation-duration, webkitAnimationDuration, WebkitAnimationDuration, -webkit-animation-duration, MozAnimationTimingFunction, -moz-animation-timing-function, webkitAnimationTimingFunction, WebkitAnimationTimingFunction, -webkit-animation-timing-function, MozAnimationIterationCount, -moz-animation-iteration-count, webkitAnimationIterationCount, WebkitAnimationIterationCount, -webkit-animation-iteration-count, MozAnimationDirection, -moz-animation-direction, webkitAnimationDirection, WebkitAnimationDirection, -webkit-animation-direction, MozAnimationPlayState, -moz-animation-play-state, webkitAnimationPlayState, WebkitAnimationPlayState, -webkit-animation-play-state, MozAnimationFillMode, -moz-animation-fill-mode, webkitAnimationFillMode, WebkitAnimationFillMode, -webkit-animation-fill-mode, MozAnimationDelay, -moz-animation-delay, webkitAnimationDelay, WebkitAnimationDelay, -webkit-animation-delay, webkitBoxAlign, WebkitBoxAlign, -webkit-box-align, webkitBoxDirection, WebkitBoxDirection, -webkit-box-direction, webkitBoxFlex, WebkitBoxFlex, -webkit-box-flex, webkitBoxOrient, WebkitBoxOrient, -webkit-box-orient, webkitBoxPack, WebkitBoxPack, -webkit-box-pack, webkitBoxOrdinalGroup, WebkitBoxOrdinalGroup, -webkit-box-ordinal-group, MozBorderStart, -moz-border-start, MozBorderEnd, -moz-border-end, webkitBorderRadius, WebkitBorderRadius, -webkit-border-radius, MozBorderImage, -moz-border-image, webkitBorderImage, WebkitBorderImage, -webkit-border-image, webkitFlexFlow, WebkitFlexFlow, -webkit-flex-flow, webkitFlex, WebkitFlex, -webkit-flex, gridGap, grid-gap, webkitMask, WebkitMask, -webkit-mask, webkitMaskPosition, WebkitMaskPosition, -webkit-mask-position, MozTransition, -moz-transition, webkitTransition, WebkitTransition, -webkit-transition, MozAnimation, -moz-animation, webkitAnimation, WebkitAnimation, -webkit-animation, constructor`, - jsKeys: 'Object.assign, Object.getPrototypeOf, Object.setPrototypeOf, Object.getOwnPropertyDescriptor, Object.getOwnPropertyDescriptors, Object.keys, Object.values, Object.entries, Object.is, Object.defineProperty, Object.defineProperties, Object.create, Object.getOwnPropertyNames, Object.getOwnPropertySymbols, Object.isExtensible, Object.preventExtensions, Object.freeze, Object.isFrozen, Object.seal, Object.isSealed, Object.fromEntries, Object.hasOwn, Object.toString, Object.toLocaleString, Object.valueOf, Object.hasOwnProperty, Object.isPrototypeOf, Object.propertyIsEnumerable, Object.__defineGetter__, Object.__defineSetter__, Object.__lookupGetter__, Object.__lookupSetter__, Object.__proto__, Function.toString, Function.apply, Function.call, Function.bind, Boolean.toString, Boolean.valueOf, Symbol.for, Symbol.keyFor, Symbol.isConcatSpreadable, Symbol.iterator, Symbol.match, Symbol.replace, Symbol.search, Symbol.species, Symbol.hasInstance, Symbol.split, Symbol.toPrimitive, Symbol.toStringTag, Symbol.unscopables, Symbol.asyncIterator, Symbol.matchAll, Symbol.toString, Symbol.valueOf, Symbol.description, Error.toString, Error.message, Error.stack, Number.isFinite, Number.isInteger, Number.isNaN, Number.isSafeInteger, Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY, Number.MAX_VALUE, Number.MIN_VALUE, Number.MAX_SAFE_INTEGER, Number.MIN_SAFE_INTEGER, Number.EPSILON, Number.parseInt, Number.parseFloat, Number.NaN, Number.toString, Number.toLocaleString, Number.valueOf, Number.toFixed, Number.toExponential, Number.toPrecision, BigInt.asUintN, BigInt.asIntN, BigInt.valueOf, BigInt.toString, BigInt.toLocaleString, Math.abs, Math.acos, Math.asin, Math.atan, Math.atan2, Math.ceil, Math.clz32, Math.cos, Math.exp, Math.floor, Math.imul, Math.fround, Math.log, Math.max, Math.min, Math.pow, Math.random, Math.round, Math.sin, Math.sqrt, Math.tan, Math.log10, Math.log2, Math.log1p, Math.expm1, Math.cosh, Math.sinh, Math.tanh, Math.acosh, Math.asinh, Math.atanh, Math.hypot, Math.trunc, Math.sign, Math.cbrt, Math.E, Math.LOG2E, Math.LOG10E, Math.LN2, Math.LN10, Math.PI, Math.SQRT2, Math.SQRT1_2, Date.UTC, Date.parse, Date.now, Date.getTime, Date.getTimezoneOffset, Date.getYear, Date.getFullYear, Date.getUTCFullYear, Date.getMonth, Date.getUTCMonth, Date.getDate, Date.getUTCDate, Date.getDay, Date.getUTCDay, Date.getHours, Date.getUTCHours, Date.getMinutes, Date.getUTCMinutes, Date.getSeconds, Date.getUTCSeconds, Date.getMilliseconds, Date.getUTCMilliseconds, Date.setTime, Date.setYear, Date.setFullYear, Date.setUTCFullYear, Date.setMonth, Date.setUTCMonth, Date.setDate, Date.setUTCDate, Date.setHours, Date.setUTCHours, Date.setMinutes, Date.setUTCMinutes, Date.setSeconds, Date.setUTCSeconds, Date.setMilliseconds, Date.setUTCMilliseconds, Date.toUTCString, Date.toLocaleString, Date.toLocaleDateString, Date.toLocaleTimeString, Date.toDateString, Date.toTimeString, Date.toISOString, Date.toJSON, Date.toString, Date.valueOf, Date.toGMTString, String.fromCharCode, String.fromCodePoint, String.raw, String.toString, String.valueOf, String.toLowerCase, String.toUpperCase, String.charAt, String.charCodeAt, String.substring, String.padStart, String.padEnd, String.codePointAt, String.includes, String.indexOf, String.lastIndexOf, String.startsWith, String.endsWith, String.trim, String.trimStart, String.trimEnd, String.toLocaleLowerCase, String.toLocaleUpperCase, String.localeCompare, String.repeat, String.normalize, String.match, String.matchAll, String.search, String.replace, String.replaceAll, String.split, String.substr, String.concat, String.slice, String.at, String.bold, String.italics, String.fixed, String.strike, String.small, String.big, String.blink, String.sup, String.sub, String.anchor, String.link, String.fontcolor, String.fontsize, String.trimLeft, String.trimRight, RegExp.input, RegExp.lastMatch, RegExp.lastParen, RegExp.leftContext, RegExp.rightContext, RegExp.$1, RegExp.$2, RegExp.$3, RegExp.$4, RegExp.$5, RegExp.$6, RegExp.$7, RegExp.$8, RegExp.$9, RegExp.$_, RegExp.$&, RegExp.$+, RegExp.$`, RegExp.$\', RegExp.toString, RegExp.compile, RegExp.exec, RegExp.test, RegExp.flags, RegExp.hasIndices, RegExp.global, RegExp.ignoreCase, RegExp.multiline, RegExp.dotAll, RegExp.source, RegExp.sticky, RegExp.unicode, Array.isArray, Array.from, Array.of, Array.toString, Array.toLocaleString, Array.join, Array.reverse, Array.sort, Array.push, Array.pop, Array.shift, Array.unshift, Array.splice, Array.concat, Array.slice, Array.lastIndexOf, Array.indexOf, Array.forEach, Array.map, Array.filter, Array.reduce, Array.reduceRight, Array.some, Array.every, Array.find, Array.findIndex, Array.copyWithin, Array.fill, Array.entries, Array.keys, Array.values, Array.includes, Array.flatMap, Array.flat, Array.at, Array.findLast, Array.findLastIndex, Map.get, Map.has, Map.set, Map.delete, Map.keys, Map.values, Map.clear, Map.forEach, Map.entries, Map.size, Set.has, Set.add, Set.delete, Set.entries, Set.clear, Set.forEach, Set.values, Set.keys, Set.size, WeakMap.has, WeakMap.get, WeakMap.delete, WeakMap.set, WeakSet.add, WeakSet.delete, WeakSet.has, Atomics.compareExchange, Atomics.load, Atomics.store, Atomics.exchange, Atomics.add, Atomics.sub, Atomics.and, Atomics.or, Atomics.xor, Atomics.isLockFree, Atomics.wait, Atomics.notify, Atomics.wake, JSON.parse, JSON.stringify, Promise.all, Promise.allSettled, Promise.any, Promise.race, Promise.reject, Promise.resolve, Promise.then, Promise.catch, Promise.finally, Reflect.apply, Reflect.construct, Reflect.defineProperty, Reflect.deleteProperty, Reflect.get, Reflect.getOwnPropertyDescriptor, Reflect.getPrototypeOf, Reflect.has, Reflect.isExtensible, Reflect.ownKeys, Reflect.preventExtensions, Reflect.set, Reflect.setPrototypeOf, Proxy.revocable, Intl.getCanonicalLocales, Intl.supportedValuesOf, Intl.Collator, Intl.DateTimeFormat, Intl.DisplayNames, Intl.ListFormat, Intl.Locale, Intl.NumberFormat, Intl.PluralRules, Intl.RelativeTimeFormat, WebAssembly.compile, WebAssembly.instantiate, WebAssembly.validate, WebAssembly.compileStreaming, WebAssembly.instantiateStreaming, WebAssembly.Module, WebAssembly.Instance, WebAssembly.Memory, WebAssembly.Table, WebAssembly.Global, WebAssembly.CompileError, WebAssembly.LinkError, WebAssembly.RuntimeError, WebAssembly.Tag, WebAssembly.Exception, Document.getElementsByTagName, Document.getElementsByTagNameNS, Document.getElementsByClassName, Document.getElementById, Document.createElement, Document.createElementNS, Document.createDocumentFragment, Document.createTextNode, Document.createComment, Document.createProcessingInstruction, Document.importNode, Document.adoptNode, Document.createEvent, Document.createRange, Document.createNodeIterator, Document.createTreeWalker, Document.createCDATASection, Document.createAttribute, Document.createAttributeNS, Document.getElementsByName, Document.open, Document.close, Document.write, Document.writeln, Document.hasFocus, Document.execCommand, Document.queryCommandEnabled, Document.queryCommandIndeterm, Document.queryCommandState, Document.queryCommandSupported, Document.queryCommandValue, Document.releaseCapture, Document.mozSetImageElement, Document.clear, Document.captureEvents, Document.releaseEvents, Document.exitFullscreen, Document.mozCancelFullScreen, Document.exitPointerLock, Document.enableStyleSheetsForSet, Document.caretPositionFromPoint, Document.querySelector, Document.querySelectorAll, Document.getSelection, Document.hasStorageAccess, Document.requestStorageAccess, Document.elementFromPoint, Document.elementsFromPoint, Document.getAnimations, Document.prepend, Document.append, Document.replaceChildren, Document.createExpression, Document.createNSResolver, Document.evaluate, Document.implementation, Document.URL, Document.documentURI, Document.compatMode, Document.characterSet, Document.charset, Document.inputEncoding, Document.contentType, Document.doctype, Document.documentElement, Document.domain, Document.referrer, Document.cookie, Document.lastModified, Document.readyState, Document.title, Document.dir, Document.body, Document.head, Document.images, Document.embeds, Document.plugins, Document.links, Document.forms, Document.scripts, Document.defaultView, Document.designMode, Document.onreadystatechange, Document.onbeforescriptexecute, Document.onafterscriptexecute, Document.currentScript, Document.fgColor, Document.linkColor, Document.vlinkColor, Document.alinkColor, Document.bgColor, Document.anchors, Document.applets, Document.all, Document.fullscreen, Document.mozFullScreen, Document.fullscreenEnabled, Document.mozFullScreenEnabled, Document.onfullscreenchange, Document.onfullscreenerror, Document.onpointerlockchange, Document.onpointerlockerror, Document.hidden, Document.visibilityState, Document.onvisibilitychange, Document.selectedStyleSheetSet, Document.lastStyleSheetSet, Document.preferredStyleSheetSet, Document.styleSheetSets, Document.scrollingElement, Document.timeline, Document.rootElement, Document.activeElement, Document.styleSheets, Document.pointerLockElement, Document.fullscreenElement, Document.mozFullScreenElement, Document.adoptedStyleSheets, Document.fonts, Document.onabort, Document.onblur, Document.onfocus, Document.onauxclick, Document.onbeforeinput, Document.oncanplay, Document.oncanplaythrough, Document.onchange, Document.onclick, Document.onclose, Document.oncontextmenu, Document.oncopy, Document.oncuechange, Document.oncut, Document.ondblclick, Document.ondrag, Document.ondragend, Document.ondragenter, Document.ondragexit, Document.ondragleave, Document.ondragover, Document.ondragstart, Document.ondrop, Document.ondurationchange, Document.onemptied, Document.onended, Document.onformdata, Document.oninput, Document.oninvalid, Document.onkeydown, Document.onkeypress, Document.onkeyup, Document.onload, Document.onloadeddata, Document.onloadedmetadata, Document.onloadstart, Document.onmousedown, Document.onmouseenter, Document.onmouseleave, Document.onmousemove, Document.onmouseout, Document.onmouseover, Document.onmouseup, Document.onwheel, Document.onpaste, Document.onpause, Document.onplay, Document.onplaying, Document.onprogress, Document.onratechange, Document.onreset, Document.onresize, Document.onscroll, Document.onscrollend, Document.onsecuritypolicyviolation, Document.onseeked, Document.onseeking, Document.onselect, Document.onslotchange, Document.onstalled, Document.onsubmit, Document.onsuspend, Document.ontimeupdate, Document.onvolumechange, Document.onwaiting, Document.onselectstart, Document.onselectionchange, Document.ontoggle, Document.onpointercancel, Document.onpointerdown, Document.onpointerup, Document.onpointermove, Document.onpointerout, Document.onpointerover, Document.onpointerenter, Document.onpointerleave, Document.ongotpointercapture, Document.onlostpointercapture, Document.onmozfullscreenchange, Document.onmozfullscreenerror, Document.onanimationcancel, Document.onanimationend, Document.onanimationiteration, Document.onanimationstart, Document.ontransitioncancel, Document.ontransitionend, Document.ontransitionrun, Document.ontransitionstart, Document.onwebkitanimationend, Document.onwebkitanimationiteration, Document.onwebkitanimationstart, Document.onwebkittransitionend, Document.onerror, Document.children, Document.firstElementChild, Document.lastElementChild, Document.childElementCount, Element.getAttributeNames, Element.getAttribute, Element.getAttributeNS, Element.toggleAttribute, Element.setAttribute, Element.setAttributeNS, Element.removeAttribute, Element.removeAttributeNS, Element.hasAttribute, Element.hasAttributeNS, Element.hasAttributes, Element.closest, Element.matches, Element.webkitMatchesSelector, Element.getElementsByTagName, Element.getElementsByTagNameNS, Element.getElementsByClassName, Element.insertAdjacentElement, Element.insertAdjacentText, Element.mozMatchesSelector, Element.setPointerCapture, Element.releasePointerCapture, Element.hasPointerCapture, Element.setCapture, Element.releaseCapture, Element.getAttributeNode, Element.setAttributeNode, Element.removeAttributeNode, Element.getAttributeNodeNS, Element.setAttributeNodeNS, Element.getClientRects, Element.getBoundingClientRect, Element.checkVisibility, Element.scrollIntoView, Element.scroll, Element.scrollTo, Element.scrollBy, Element.insertAdjacentHTML, Element.querySelector, Element.querySelectorAll, Element.attachShadow, Element.requestFullscreen, Element.mozRequestFullScreen, Element.requestPointerLock, Element.animate, Element.getAnimations, Element.before, Element.after, Element.replaceWith, Element.remove, Element.prepend, Element.append, Element.replaceChildren, Element.namespaceURI, Element.prefix, Element.localName, Element.tagName, Element.id, Element.className, Element.classList, Element.part, Element.attributes, Element.scrollTop, Element.scrollLeft, Element.scrollWidth, Element.scrollHeight, Element.clientTop, Element.clientLeft, Element.clientWidth, Element.clientHeight, Element.scrollTopMax, Element.scrollLeftMax, Element.innerHTML, Element.outerHTML, Element.shadowRoot, Element.assignedSlot, Element.slot, Element.onfullscreenchange, Element.onfullscreenerror, Element.previousElementSibling, Element.nextElementSibling, Element.children, Element.firstElementChild, Element.lastElementChild, Element.childElementCount', - }, - }); - // @ts-ignore - const getListDiff = ({ oldList, newList, removeCamelCase = false } = {}) => { - const oldSet = new Set(oldList); - const newSet = new Set(newList); - newList.forEach((x) => oldSet.delete(x)); - oldList.forEach((x) => newSet.delete(x)); - const camelCase = /[a-z][A-Z]/; - return { - removed: !removeCamelCase ? [...oldSet] : [...oldSet].filter((key) => !camelCase.test(key)), - added: !removeCamelCase ? [...newSet] : [...newSet].filter((key) => !camelCase.test(key)), - }; - }; - const BROWSER = (IS_BLINK ? 'Chrome' : IS_GECKO ? 'Firefox' : ''); - const getEngineMaps = (browser) => { - // Blink - const blinkJS = { - '76': ['Document.onsecuritypolicyviolation', 'Promise.allSettled'], - '77': ['Document.onformdata', 'Document.onpointerrawupdate'], - '78': ['Element.elementTiming'], - '79': ['Document.onanimationend', 'Document.onanimationiteration', 'Document.onanimationstart', 'Document.ontransitionend'], - '80': ['!Document.registerElement', '!Element.createShadowRoot', '!Element.getDestinationInsertionPoints'], - '81': ['Document.onwebkitanimationend', 'Document.onwebkitanimationiteration', 'Document.onwebkitanimationstart', 'Document.onwebkittransitionend', 'Element.ariaAtomic', 'Element.ariaAutoComplete', 'Element.ariaBusy', 'Element.ariaChecked', 'Element.ariaColCount', 'Element.ariaColIndex', 'Element.ariaColSpan', 'Element.ariaCurrent', 'Element.ariaDisabled', 'Element.ariaExpanded', 'Element.ariaHasPopup', 'Element.ariaHidden', 'Element.ariaKeyShortcuts', 'Element.ariaLabel', 'Element.ariaLevel', 'Element.ariaLive', 'Element.ariaModal', 'Element.ariaMultiLine', 'Element.ariaMultiSelectable', 'Element.ariaOrientation', 'Element.ariaPlaceholder', 'Element.ariaPosInSet', 'Element.ariaPressed', 'Element.ariaReadOnly', 'Element.ariaRelevant', 'Element.ariaRequired', 'Element.ariaRoleDescription', 'Element.ariaRowCount', 'Element.ariaRowIndex', 'Element.ariaRowSpan', 'Element.ariaSelected', 'Element.ariaSort', 'Element.ariaValueMax', 'Element.ariaValueMin', 'Element.ariaValueNow', 'Element.ariaValueText', 'Intl.DisplayNames'], - '83': ['Element.ariaDescription', 'Element.onbeforexrselect'], - '84': ['Document.getAnimations', 'Document.timeline', 'Element.ariaSetSize', 'Element.getAnimations'], - '85': ['Promise.any', 'String.replaceAll'], - '86': ['Document.fragmentDirective', 'Document.replaceChildren', 'Element.replaceChildren', '!Atomics.wake'], - '87-89': ['Atomics.waitAsync', 'Document.ontransitioncancel', 'Document.ontransitionrun', 'Document.ontransitionstart', 'Intl.Segmenter'], - '90': ['Document.onbeforexrselect', 'RegExp.hasIndices', '!Element.onbeforexrselect'], - '91': ['Element.getInnerHTML'], - '92': ['Array.at', 'String.at'], - '93': ['Error.cause', 'Object.hasOwn'], - '94': ['!Error.cause', 'Object.hasOwn'], - '95-96': ['WebAssembly.Exception', 'WebAssembly.Tag'], - '97-98': ['Array.findLast', 'Array.findLastIndex', 'Document.onslotchange'], - '99-101': ['Intl.supportedValuesOf', 'Document.oncontextlost', 'Document.oncontextrestored'], - '102': ['Element.ariaInvalid', 'Document.onbeforematch'], - '103-106': ['Element.role'], - '107-109': ['Element.ariaBrailleLabel', 'Element.ariaBrailleRoleDescription'], - '110': ['Array.toReversed', 'Array.toSorted', 'Array.toSpliced', 'Array.with'], - '111': ['String.isWellFormed', 'String.toWellFormed', 'Document.startViewTransition'], - '112-113': ['RegExp.unicodeSets'], - '114-115': ['JSON.rawJSON', 'JSON.isRawJSON'], - }; - const blinkCSS = { - '76': ['backdrop-filter'], - '77-80': ['overscroll-behavior-block', 'overscroll-behavior-inline'], - '81': ['color-scheme', 'image-orientation'], - '83': ['contain-intrinsic-size'], - '84': ['appearance', 'ruby-position'], - '85-86': ['content-visibility', 'counter-set', 'inherits', 'initial-value', 'page-orientation', 'syntax'], - '87': ['ascent-override', 'border-block', 'border-block-color', 'border-block-style', 'border-block-width', 'border-inline', 'border-inline-color', 'border-inline-style', 'border-inline-width', 'descent-override', 'inset', 'inset-block', 'inset-block-end', 'inset-block-start', 'inset-inline', 'inset-inline-end', 'inset-inline-start', 'line-gap-override', 'margin-block', 'margin-inline', 'padding-block', 'padding-inline', 'text-decoration-thickness', 'text-underline-offset'], - '88': ['aspect-ratio'], - '89': ['border-end-end-radius', 'border-end-start-radius', 'border-start-end-radius', 'border-start-start-radius', 'forced-color-adjust'], - '90': ['overflow-clip-margin'], - '91': ['additive-symbols', 'fallback', 'negative', 'pad', 'prefix', 'range', 'speak-as', 'suffix', 'symbols', 'system'], - '92': ['size-adjust'], - '93': ['accent-color'], - '94': ['scrollbar-gutter'], - '95-96': ['app-region', 'contain-intrinsic-block-size', 'contain-intrinsic-height', 'contain-intrinsic-inline-size', 'contain-intrinsic-width'], - '97-98': ['font-synthesis-small-caps', 'font-synthesis-style', 'font-synthesis-weight', 'font-synthesis'], - '99-100': ['text-emphasis-color', 'text-emphasis-position', 'text-emphasis-style', 'text-emphasis'], - '101-103': ['font-palette', 'base-palette', 'override-colors'], - '104': ['object-view-box'], - '105': ['container-name', 'container-type', 'container'], - '106-107': ['hyphenate-character'], - '108': ['hyphenate-character', '!orientation', '!max-zoom', '!min-zoom', '!user-zoom'], - '109': ['hyphenate-limit-chars', 'math-depth', 'math-shift', 'math-style'], - '110': ['initial-letter'], - '111-113': ['baseline-source', 'font-variant-alternates', 'view-transition-name'], - '114-115': ['text-wrap', 'white-space-collapse'], - }; - const blinkWindow = { - '80': ['CompressionStream', 'DecompressionStream', 'FeaturePolicy', 'FragmentDirective', 'PeriodicSyncManager', 'VideoPlaybackQuality'], - '81': ['SubmitEvent', 'XRHitTestResult', 'XRHitTestSource', 'XRRay', 'XRTransientInputHitTestResult', 'XRTransientInputHitTestSource'], - '83': ['BarcodeDetector', 'XRDOMOverlayState', 'XRSystem'], - '84': ['AnimationPlaybackEvent', 'AnimationTimeline', 'CSSAnimation', 'CSSTransition', 'DocumentTimeline', 'FinalizationRegistry', 'LayoutShiftAttribution', 'ResizeObserverSize', 'WakeLock', 'WakeLockSentinel', 'WeakRef', 'XRLayer'], - '85': ['AggregateError', 'CSSPropertyRule', 'EventCounts', 'XRAnchor', 'XRAnchorSet'], - '86': ['RTCEncodedAudioFrame', 'RTCEncodedVideoFrame'], - '87': ['CookieChangeEvent', 'CookieStore', 'CookieStoreManager', 'Scheduling'], - '88': ['Scheduling', '!BarcodeDetector'], - '89': ['ReadableByteStreamController', 'ReadableStreamBYOBReader', 'ReadableStreamBYOBRequest', 'ReadableStreamDefaultController', 'XRWebGLBinding'], - '90': ['AbstractRange', 'CustomStateSet', 'NavigatorUAData', 'XRCPUDepthInformation', 'XRDepthInformation', 'XRLightEstimate', 'XRLightProbe', 'XRWebGLDepthInformation'], - '91': ['CSSCounterStyleRule', 'GravitySensor', 'NavigatorManagedData'], - '92': ['CSSCounterStyleRule', '!SharedArrayBuffer'], - '93': ['WritableStreamDefaultController'], - '94': ['AudioData', 'AudioDecoder', 'AudioEncoder', 'EncodedAudioChunk', 'EncodedVideoChunk', 'IdleDetector', 'ImageDecoder', 'ImageTrack', 'ImageTrackList', 'VideoColorSpace', 'VideoDecoder', 'VideoEncoder', 'VideoFrame', 'MediaStreamTrackGenerator', 'MediaStreamTrackProcessor', 'Profiler', 'VirtualKeyboard', 'DelegatedInkTrailPresenter', 'Ink', 'Scheduler', 'TaskController', 'TaskPriorityChangeEvent', 'TaskSignal', 'VirtualKeyboardGeometryChangeEvent'], - '95-96': ['URLPattern'], - '97-98': ['WebTransport', 'WebTransportBidirectionalStream', 'WebTransportDatagramDuplexStream', 'WebTransportError'], - '99': ['CanvasFilter', 'CSSLayerBlockRule', 'CSSLayerStatementRule'], - '100': ['CSSMathClamp'], - '101-104': ['CSSFontPaletteValuesRule'], - '105-106': ['CSSContainerRule'], - '107-108': ['XRCamera'], - '109': ['MathMLElement'], - '110': ['AudioSinkInfo'], - '111-112': ['ViewTransition'], - '113-115': ['ViewTransition', '!CanvasFilter'], - }; - // Gecko - const geckoJS = { - '71': ['Promise.allSettled'], - '72-73': ['Document.onformdata', 'Element.part'], - '74': ['!Array.toSource', '!Boolean.toSource', '!Date.toSource', '!Error.toSource', '!Function.toSource', '!Intl.toSource', '!JSON.toSource', '!Math.toSource', '!Number.toSource', '!Object.toSource', '!RegExp.toSource', '!String.toSource', '!WebAssembly.toSource'], - '75-76': ['Document.getAnimations', 'Document.timeline', 'Element.getAnimations', 'Intl.Locale'], - '77': ['String.replaceAll'], - '78': ['Atomics.add', 'Atomics.and', 'Atomics.compareExchange', 'Atomics.exchange', 'Atomics.isLockFree', 'Atomics.load', 'Atomics.notify', 'Atomics.or', 'Atomics.store', 'Atomics.sub', 'Atomics.wait', 'Atomics.wake', 'Atomics.xor', 'Document.replaceChildren', 'Element.replaceChildren', 'Intl.ListFormat', 'RegExp.dotAll'], - '79-84': ['Promise.any'], - '85': ['!Document.onshow', 'Promise.any'], - '86': ['Intl.DisplayNames'], - '87': ['Document.onbeforeinput'], - '88-89': ['RegExp.hasIndices'], - '90-91': ['Array.at', 'String.at'], - '92': ['Object.hasOwn'], - '93-99': ['Intl.supportedValuesOf', 'Document.onsecuritypolicyviolation', 'Document.onslotchange'], - '100': ['WebAssembly.Tag', 'WebAssembly.Exception'], - '101-103': ['Document.adoptedStyleSheets'], - '104-108': ['Array.findLast', 'Array.findLastIndex'], - '109-112': ['Document.onscrollend'], - }; - // {"added":[],"removed":[]} {"added":["Document.onscrollend"],"removed":[]} {"added":["onscrollend"],"removed":[]} - const geckoCSS = { - '71': ['-moz-column-span'], - '72': ['offset', 'offset-anchor', 'offset-distance', 'offset-path', 'offset-rotate', 'rotate', 'scale', 'translate'], - '73': ['overscroll-behavior-block', 'overscroll-behavior-inline'], - '74-79': ['!-moz-stack-sizing', 'text-underline-position'], - '80-88': ['appearance'], - '89-90': ['!-moz-outline-radius', '!-moz-outline-radius-bottomleft', '!-moz-outline-radius-bottomright', '!-moz-outline-radius-topleft', '!-moz-outline-radius-topright', 'aspect-ratio'], - '91': ['tab-size'], - '92-95': ['accent-color'], - '96': ['color-scheme'], - '97': ['print-color-adjust', 'scrollbar-gutter', 'd'], - '98-101': ['hyphenate-character'], - '102': ['overflow-clip-margin'], - '103-106': ['scroll-snap-stop'], - '107-108': ['backdrop-filter', 'font-palette', 'contain-intrinsic-block-size', 'contain-intrinsic-height', 'contain-intrinsic-inline-size', 'contain-intrinsic-width', 'contain-intrinsic-size'], - '109': ['-webkit-clip-path'], - '110': ['container-type', 'container-name', 'page', 'container'], - '111': ['font-synthesis-small-caps', 'font-synthesis-style', 'font-synthesis-weight'], - '112': ['font-synthesis-small-caps', '!-moz-image-region'], - }; - const geckoWindow = { - // disregard: 'reportError','onsecuritypolicyviolation','onslotchange' - '71': ['MathMLElement', '!SVGZoomAndPan'], - '72-73': ['!BatteryManager', 'FormDataEvent', 'Geolocation', 'GeolocationCoordinates', 'GeolocationPosition', 'GeolocationPositionError', '!mozPaintCount'], - '74': ['FormDataEvent', '!uneval'], - '75': ['AnimationTimeline', 'CSSAnimation', 'CSSTransition', 'DocumentTimeline', 'SubmitEvent'], - '76-77': ['AudioParamMap', 'AudioWorklet', 'AudioWorkletNode', 'Worklet'], - '78': ['Atomics'], - '79-81': ['AggregateError', 'FinalizationRegistry'], - '82': ['MediaMetadata', 'MediaSession', 'Sanitizer'], - '83': ['MediaMetadata', 'MediaSession', '!Sanitizer'], - '84': ['PerformancePaintTiming'], - '85-86': ['PerformancePaintTiming', '!HTMLMenuItemElement', '!onshow'], - '87': ['onbeforeinput'], - '88': ['onbeforeinput', '!VisualViewport'], - '89-92': ['!ondevicelight', '!ondeviceproximity', '!onuserproximity'], - '93-95': ['ElementInternals'], - '96': ['Lock', 'LockManager'], - '97': ['CSSLayerBlockRule', 'CSSLayerStatementRule'], - '98': ['HTMLDialogElement'], - '99': ['NavigationPreloadManager'], - '100-104': ['WritableStream'], - '105-106': ['TextDecoderStream', 'OffscreenCanvasRenderingContext2D', 'OffscreenCanvas', 'TextEncoderStream'], - '107-109': ['CSSFontPaletteValuesRule'], - '110': ['CSSContainerRule'], - '111': ['FileSystemFileHandle', 'FileSystemDirectoryHandle'], - '112': ['FileSystemFileHandle', '!U2F'], - }; - const IS_BLINK = browser == 'Chrome'; - const IS_GECKO = browser == 'Firefox'; - const css = (IS_BLINK ? blinkCSS : IS_GECKO ? geckoCSS : {}); - const win = (IS_BLINK ? blinkWindow : IS_GECKO ? geckoWindow : {}); - const js = (IS_BLINK ? blinkJS : IS_GECKO ? geckoJS : {}); - return { - css, - win, - js, - }; - }; - const getJSCoreFeatures = (win) => { - const globalObjects = [ - 'Object', - 'Function', - 'Boolean', - 'Symbol', - 'Error', - 'Number', - 'BigInt', - 'Math', - 'Date', - 'String', - 'RegExp', - 'Array', - 'Map', - 'Set', - 'WeakMap', - 'WeakSet', - 'Atomics', - 'JSON', - 'Promise', - 'Reflect', - 'Proxy', - 'Intl', - 'WebAssembly', - 'Document', - 'Element', - ]; - try { - // @ts-ignore - const features = globalObjects.reduce((acc, name) => { - const ignore = ['name', 'length', 'constructor', 'prototype', 'arguments', 'caller']; - const descriptorKeys = Object.keys(Object.getOwnPropertyDescriptors(win[name] || {})); - const descriptorProtoKeys = Object.keys(Object.getOwnPropertyDescriptors((win[name] || {}).prototype || {})); - const uniques = [...new Set([...descriptorKeys, ...descriptorProtoKeys].filter((key) => !ignore.includes(key)))]; - const keys = uniques.map((key) => `${name}.${key}`); - return [...acc, ...keys]; - }, []); - return features; - } - catch (error) { - console.error(error); - return []; - } - }; - // @ts-ignore - const versionSort = (x) => x.sort((a, b) => /\d+/.exec(a)[0] - /\d+/.exec(b)[0]).reverse(); - const getVersionLie = (vReport, version, forgivenessOffset = 0) => { - const stable = getStableFeatures(); - const { version: maxVersion } = stable[BROWSER] || {}; - const validMetrics = vReport && version; - if (!validMetrics) { - return {}; - } - const [vStart, vEnd] = version ? version.split('-') : []; - const vMax = (vEnd || vStart); - const reportIsTooHigh = +vReport > (+vMax + forgivenessOffset); - const reportIsTooLow = +vReport < (+vStart - forgivenessOffset); - const reportIsOff = (reportIsTooHigh || reportIsTooLow); - const versionIsAboveMax = ((+vMax == maxVersion) && - (+vReport > maxVersion)); - const liedVersion = !versionIsAboveMax && reportIsOff; - const distance = !liedVersion ? 0 : (Math.abs(vReport - (reportIsTooLow ? vStart : vMax))); - return { liedVersion, distance }; - }; - async function getEngineFeatures({ cssComputed, navigatorComputed, windowFeaturesComputed, }) { - try { - const timer = createTimer(); - await queueEvent(timer); - const win = PHANTOM_DARKNESS ? PHANTOM_DARKNESS : window; - if (!cssComputed || !windowFeaturesComputed) { - logTestResult({ test: 'features', passed: false }); - return; - } - const jsFeaturesKeys = getJSCoreFeatures(win); - const { keys: computedStyleKeys } = cssComputed.computedStyle || {}; - const { keys: windowFeaturesKeys } = windowFeaturesComputed || {}; - const { userAgentParsed: decryptedName } = navigatorComputed || {}; - const isNative = (win, x) => (/\[native code\]/.test(win[x] + '') && - 'prototype' in win[x] && - win[x].prototype.constructor.name === x); - // @ts-ignore - const getFeatures = ({ context, allKeys, engineMap, checkNative = false } = {}) => { - const allKeysSet = new Set(allKeys); - const features = new Set(); - // @ts-ignore - const match = Object.keys(engineMap || {}).reduce((acc, key) => { - const version = engineMap[key]; - const versionLen = version.length; - const featureLen = version.filter((prop) => { - const removedFromVersion = prop.charAt(0) == '!'; - if (removedFromVersion) { - const propName = prop.slice(1); - return !allKeysSet.has(propName) && features.add(prop); - } - return (allKeysSet.has(prop) && - (checkNative ? isNative(context, prop) : true) && - features.add(prop)); - }).length; - return versionLen == featureLen ? [...acc, key] : acc; - }, []); - const version = versionSort(match)[0]; - return { - version, - features, - }; - }; - // engine maps - const { css: engineMapCSS, win: engineMapWindow, js: engineMapJS, } = getEngineMaps(BROWSER); - // css version - const { version: cssVersion, features: cssFeatures, } = getFeatures({ - context: win, - allKeys: computedStyleKeys, - engineMap: engineMapCSS, - }); - // window version - const { version: windowVersion, features: windowFeatures, } = getFeatures({ - context: win, - allKeys: windowFeaturesKeys, - engineMap: engineMapWindow, - checkNative: true, - }); - // js version - const { version: jsVersion, features: jsFeatures, } = getFeatures({ - context: win, - allKeys: jsFeaturesKeys, - engineMap: engineMapJS, - }); - // determine version based on 3 factors - const getVersionFromRange = (range, versionCollection) => { - const exactVersion = versionCollection.find((version) => version && !/-/.test(version)); - if (exactVersion) { - return exactVersion; - } - const len = range.length; - const first = range[0]; - const last = range[len - 1]; - return (!len ? '' : - len == 1 ? first : - `${last}-${first}`); - }; - const versionSet = new Set([ - cssVersion, - windowVersion, - jsVersion, - ]); - versionSet.delete(undefined); - const versionRange = versionSort([...versionSet].reduce((acc, x) => [...acc, ...x.split('-')], [])); - const version = getVersionFromRange(versionRange, [cssVersion, windowVersion, jsVersion]); - const vReport = (/\d+/.exec(decryptedName) || [])[0]; - const { liedVersion: liedCSS, distance: distanceCSS, } = getVersionLie(vReport, cssVersion); - const { liedVersion: liedJS, distance: distanceJS, } = getVersionLie(vReport, jsVersion); - const { liedVersion: liedWindow, distance: distanceWindow, } = getVersionLie(vReport, windowVersion); - if (liedCSS) { - sendToTrash('userAgent', `v${vReport} failed v${cssVersion} CSS features`); - if (distanceCSS > 1) { - documentLie(`Navigator.userAgent`, `v${vReport} failed CSS features by ${distanceCSS} versions`); - } - } - if (liedJS) { - sendToTrash('userAgent', `v${vReport} failed v${jsVersion} JS features`); - if (distanceJS > 2) { - documentLie(`Navigator.userAgent`, `v${vReport} failed JS features by ${distanceJS} versions`); - } - } - if (liedWindow) { - sendToTrash('userAgent', `v${vReport} failed v${windowVersion} Window features`); - if (distanceWindow > 3) { - documentLie(`Navigator.userAgent`, `v${vReport} failed Window features by ${distanceWindow} versions`); - } - } - logTestResult({ time: timer.stop(), test: 'features', passed: true }); - return { - versionRange, - version, - cssVersion, - windowVersion, - jsVersion, - cssFeatures: [...cssFeatures], - windowFeatures: [...windowFeatures], - jsFeatures: [...jsFeatures], - jsFeaturesKeys, - }; - } - catch (error) { - logTestResult({ test: 'features', passed: false }); - captureError(error); - return; - } - } - function featuresHTML(fp) { - if (!fp.features) { - return ` -
-
Features: ${HTMLNote.UNKNOWN}
-
JS/DOM: ${HTMLNote.UNKNOWN}
-
-
-
CSS: ${HTMLNote.UNKNOWN}
-
Window: ${HTMLNote.UNKNOWN}
-
`; - } - const { versionRange, version, cssVersion, jsVersion, windowVersion, cssFeatures, windowFeatures, jsFeatures, jsFeaturesKeys, } = fp.features || {}; - const { keys: windowFeaturesKeys } = fp.windowFeatures || {}; - const { keys: computedStyleKeys } = fp.css.computedStyle || {}; - const { userAgentVersion } = fp.workerScope || {}; - const { css: engineMapCSS, win: engineMapWindow, js: engineMapJS, } = getEngineMaps(BROWSER); - // logger - const shouldLogFeatures = (browser, version, userAgentVersion) => { - const shouldLog = userAgentVersion > version; - return shouldLog; - }; - const log = ({ features, name, diff }) => { - console.groupCollapsed(`%c ${name} Features %c-${diff.removed.length} %c+${diff.added.length}`, 'color: #4cc1f9', 'color: Salmon', 'color: MediumAquaMarine'); - Object.keys(diff).forEach((key) => { - console.log(`%c${key}:`, `color: ${key == 'added' ? 'MediumAquaMarine' : 'Salmon'}`); - return console.log(diff[key].join('\n')); - }); - console.log(features.join(', ')); - return console.groupEnd(); - }; - // modal - const report = { computedStyleKeys, windowFeaturesKeys, jsFeaturesKeys }; - const getModal = ({ id, engineMap, features, browser, report, userAgentVersion }) => { - // capture diffs from stable release - const stable = getStableFeatures(); - const { windowKeys, cssKeys, jsKeys, version } = stable[browser] || {}; - const logger = shouldLogFeatures(browser, version, userAgentVersion); - let diff = null; - if (id == 'css') { - const { computedStyleKeys } = report; - if (cssKeys) { - diff = getListDiff({ - oldList: cssKeys.split(', '), - newList: computedStyleKeys, - removeCamelCase: true, - }); - } - if (logger) { - console.log(`computing ${browser} ${userAgentVersion} diffs from ${browser} ${version}...`); - Analysis.featuresCSS = diff; - log({ features: computedStyleKeys, name: 'CSS', diff }); - } - } - else if (id == 'window') { - const { windowFeaturesKeys } = report; - if (windowKeys) { - diff = getListDiff({ - oldList: windowKeys.split(', '), - newList: windowFeaturesKeys, - }); - } - if (logger) { - Analysis.featuresWindow = diff; - log({ features: windowFeaturesKeys, name: 'Window', diff }); - } - } - else if (id == 'js') { - const { jsFeaturesKeys } = report; - if (jsKeys) { - diff = getListDiff({ - oldList: jsKeys.split(', '), - newList: jsFeaturesKeys, - }); - } - if (logger) { - Analysis.featuresJS = diff; - log({ features: jsFeaturesKeys, name: 'JS', diff }); - } - } - const header = !version || !diff || (!diff.added.length && !diff.removed.length) ? '' : ` - diffs from ${version}: -
- ${diff && diff.added.length ? - diff.added.map((key) => `
${key}
`).join('') : ''} - ${diff && diff.removed.length ? - diff.removed.map((key) => `
${key}
`).join('') : ''} -
- - `; - return modal(`creep-features-${id}`, header + versionSort(Object.keys(engineMap)).map((key) => { - return ` - ${key}:
${engineMap[key].map((prop) => { - return `${prop}`; - }).join('
')} - `; - }).join('
'), hashMini([...features])); - }; - Analysis.featuresVersion = +userAgentVersion || 0; - const cssModal = getModal({ - id: 'css', - engineMap: engineMapCSS, - features: new Set(cssFeatures), - browser: BROWSER, - report, - userAgentVersion, - }); - const windowModal = getModal({ - id: 'window', - engineMap: engineMapWindow, - features: new Set(windowFeatures), - browser: BROWSER, - report, - userAgentVersion, - }); - const jsModal = getModal({ - id: 'js', - engineMap: engineMapJS, - features: new Set(jsFeatures), - browser: BROWSER, - report, - userAgentVersion, - }); - const getIcon = (name) => ``; - const browserIcon = (!BROWSER ? '' : - /chrome/i.test(BROWSER) ? getIcon('chrome') : - /firefox/i.test(BROWSER) ? getIcon('firefox') : - ''); - return ` - - ${performanceLogger.getLog().features} -
-
Features: ${versionRange.length ? `${browserIcon}${version}+` : HTMLNote.UNKNOWN}
-
JS/DOM: ${jsVersion ? `${jsModal} (v${jsVersion})` : HTMLNote.UNKNOWN}
-
-
-
CSS: ${cssVersion ? `${cssModal} (v${cssVersion})` : HTMLNote.UNKNOWN}
-
Window: ${windowVersion ? `${windowModal} (v${windowVersion})` : HTMLNote.UNKNOWN}
-
- `; - } - - // inspired by Lalit Patel's fontdetect.js - // https://www.lalit.org/wordpress/wp-content/uploads/2008/05/fontdetect.js?ver=0.3 - const WindowsFonts = { - // https://docs.microsoft.com/en-us/typography/fonts/windows_11_font_list - '7': [ - 'Cambria Math', - 'Lucida Console', - ], - '8': [ - 'Aldhabi', - 'Gadugi', - 'Myanmar Text', - 'Nirmala UI', - ], - '8.1': [ - 'Leelawadee UI', - 'Javanese Text', - 'Segoe UI Emoji', - ], - '10': [ - 'HoloLens MDL2 Assets', // 10 (v1507) + - 'Segoe MDL2 Assets', // 10 (v1507) + - 'Bahnschrift', // 10 (v1709) +- - 'Ink Free', // 10 (v1803) +- - ], - '11': ['Segoe Fluent Icons'], - }; - const MacOSFonts = { - // Mavericks and below - '10.9': [ - 'Helvetica Neue', - 'Geneva', // mac (not iOS) - ], - // Yosemite - '10.10': [ - 'Kohinoor Devanagari Medium', - 'Luminari', - ], - // El Capitan - '10.11': [ - 'PingFang HK Light', - ], - // Sierra: https://support.apple.com/en-ie/HT206872 - '10.12': [ - 'American Typewriter Semibold', - 'Futura Bold', - 'SignPainter-HouseScript Semibold', - ], - // High Sierra: https://support.apple.com/en-me/HT207962 - // Mojave: https://support.apple.com/en-us/HT208968 - '10.13-10.14': [ - 'InaiMathi Bold', - ], - // Catalina: https://support.apple.com/en-us/HT210192 - // Big Sur: https://support.apple.com/en-sg/HT211240 - '10.15-11': [ - 'Galvji', - 'MuktaMahee Regular', - ], - // Monterey: https://support.apple.com/en-us/HT212587 - '12': [ - 'Noto Sans Gunjala Gondi Regular', - 'Noto Sans Masaram Gondi Regular', - 'Noto Serif Yezidi Regular' - ], - // Ventura: https://support.apple.com/en-us/HT213266 - '13': [ - 'Apple SD Gothic Neo ExtraBold', - 'STIX Two Math Regular', - 'STIX Two Text Regular', - 'Noto Sans Canadian Aboriginal Regular', - ], - }; - const DesktopAppFonts = { - // docs.microsoft.com/en-us/typography/font-list/ms-outlook - 'Microsoft Outlook': ['MS Outlook'], - // https://community.adobe.com/t5/postscript-discussions/zwadobef-font/m-p/3730427#M785 - 'Adobe Acrobat': ['ZWAdobeF'], - // https://wiki.documentfoundation.org/Fonts - 'LibreOffice': [ - 'Amiri', - 'KACSTOffice', - 'Liberation Mono', - 'Source Code Pro', - ], - // https://superuser.com/a/611804 - 'OpenOffice': [ - 'DejaVu Sans', - 'Gentium Book Basic', - 'OpenSymbol', - ], - }; - const APPLE_FONTS = Object.keys(MacOSFonts).map((key) => MacOSFonts[key]).flat(); - const WINDOWS_FONTS = Object.keys(WindowsFonts).map((key) => WindowsFonts[key]).flat(); - const DESKTOP_APP_FONTS = (Object.keys(DesktopAppFonts).map((key) => DesktopAppFonts[key]).flat()); - const LINUX_FONTS = [ - 'Arimo', // ubuntu, chrome os - 'Chilanka', // ubuntu (not TB) - 'Cousine', // ubuntu, chrome os - 'Jomolhari', // chrome os - 'MONO', // ubuntu, chrome os (not TB) - 'Noto Color Emoji', // Linux - 'Ubuntu', // ubuntu (not TB) - ]; - const ANDROID_FONTS = [ - 'Dancing Script', // android - 'Droid Sans Mono', // Android - 'Roboto', // Android, Chrome OS - ]; - const FONT_LIST = [ - ...APPLE_FONTS, - ...WINDOWS_FONTS, - ...LINUX_FONTS, - ...ANDROID_FONTS, - ...DESKTOP_APP_FONTS, - ].sort(); - async function getFonts() { - const getPixelEmojis = ({ doc, id, emojis }) => { - try { - patch(doc.getElementById(id), html ` -
- - ${emojis.map((emoji) => { - return `
${emoji}
`; - }).join('')} -
- `); - // get emoji set and system - const getEmojiDimensions = (style) => { - return { - width: style.inlineSize, - height: style.blockSize, - }; - }; - const pattern = new Set(); - const emojiElems = [...doc.getElementsByClassName('pixel-emoji')]; - const emojiSet = emojiElems.reduce((emojiSet, el, i) => { - const style = getComputedStyle(el); - const emoji = emojis[i]; - const { height, width } = getEmojiDimensions(style); - const dimensions = `${width},${height}`; - if (!pattern.has(dimensions)) { - pattern.add(dimensions); - emojiSet.add(emoji); - } - return emojiSet; - }, new Set()); - const pixelToNumber = (pixels) => +(pixels.replace('px', '')); - const pixelSizeSystemSum = 0.00001 * [...pattern].map((x) => { - return x.split(',').map((x) => pixelToNumber(x)).reduce((acc, x) => acc += (+x || 0), 0); - }).reduce((acc, x) => acc += x, 0); - doc.body.removeChild(doc.getElementById('pixel-emoji-container')); - return { - emojiSet: [...emojiSet], - pixelSizeSystemSum, - }; - } - catch (error) { - console.error(error); - return { - emojiSet: [], - pixelSizeSystemSum: 0, - }; - } - }; - const getFontFaceLoadFonts = async (fontList) => { - try { - let fontsChecked = []; - if (!document.fonts.check(`0px "${getRandomValues()}"`)) { - fontsChecked = fontList.reduce((acc, font) => { - const found = document.fonts.check(`0px "${font}"`); - if (found) - acc.push(font); - return acc; - }, []); - } - const fontFaceList = fontList.map((font) => new FontFace(font, `local("${font}")`)); - const responseCollection = await Promise - .allSettled(fontFaceList.map((font) => font.load())); - const fontsLoaded = responseCollection.reduce((acc, font) => { - if (font.status == 'fulfilled') { - acc.push(font.value.family); - } - return acc; - }, []); - return [...new Set([...fontsChecked, ...fontsLoaded])].sort(); - } - catch (error) { - console.error(error); - return []; - } - }; - const getPlatformVersion = (fonts) => { - const getWindows = ({ fonts, fontMap }) => { - const fontVersion = { - ['11']: fontMap['11'].find((x) => fonts.includes(x)), - ['10']: fontMap['10'].find((x) => fonts.includes(x)), - ['8.1']: fontMap['8.1'].find((x) => fonts.includes(x)), - ['8']: fontMap['8'].find((x) => fonts.includes(x)), - // require complete set of Windows 7 fonts - ['7']: fontMap['7'].filter((x) => fonts.includes(x)).length == fontMap['7'].length, - }; - const hash = ('' + Object.keys(fontVersion).sort().filter((key) => !!fontVersion[key])); - const hashMap = { - '10,11,7,8,8.1': '11', - '10,7,8,8.1': '10', - '7,8,8.1': '8.1', - '11,7,8,8.1': '8.1', // missing 10 - '7,8': '8', - '10,7,8': '8', // missing 8.1 - '10,11,7,8': '8', // missing 8.1 - '7': '7', - '7,8.1': '7', - '10,7,8.1': '7', // missing 8 - '10,11,7,8.1': '7', // missing 8 - }; - const version = hashMap[hash]; - return version ? `Windows ${version}` : undefined; - }; - const getMacOS = ({ fonts, fontMap }) => { - const fontVersion = { - ['13']: fontMap['13'].find((x) => fonts.includes(x)), - ['12']: fontMap['12'].find((x) => fonts.includes(x)), - ['10.15-11']: fontMap['10.15-11'].find((x) => fonts.includes(x)), - ['10.13-10.14']: fontMap['10.13-10.14'].find((x) => fonts.includes(x)), - ['10.12']: fontMap['10.12'].find((x) => fonts.includes(x)), - ['10.11']: fontMap['10.11'].find((x) => fonts.includes(x)), - ['10.10']: fontMap['10.10'].find((x) => fonts.includes(x)), - // require complete set of 10.9 fonts - ['10.9']: fontMap['10.9'].filter((x) => fonts.includes(x)).length == fontMap['10.9'].length, - }; - const hash = ('' + Object.keys(fontVersion).sort().filter((key) => !!fontVersion[key])); - const hashMap = { - '10.10,10.11,10.12,10.13-10.14,10.15-11,10.9,12,13': 'Ventura', - '10.10,10.11,10.12,10.13-10.14,10.15-11,10.9,12': 'Monterey', - '10.10,10.11,10.12,10.13-10.14,10.15-11,10.9': '10.15-11', - '10.10,10.11,10.12,10.13-10.14,10.9': '10.13-10.14', - '10.10,10.11,10.12,10.9': 'Sierra', // 10.12 - '10.10,10.11,10.9': 'El Capitan', // 10.11 - '10.10,10.9': 'Yosemite', // 10.10 - '10.9': 'Mavericks', // 10.9 - }; - const version = hashMap[hash]; - return version ? `macOS ${version}` : undefined; - }; - return (getWindows({ fonts, fontMap: WindowsFonts }) || - getMacOS({ fonts, fontMap: MacOSFonts })); - }; - const getDesktopApps = (fonts) => { - // @ts-ignore - const apps = Object.keys(DesktopAppFonts).reduce((acc, key) => { - const appFontSet = DesktopAppFonts[key]; - const match = appFontSet.filter((x) => fonts.includes(x)).length == appFontSet.length; - return match ? [...acc, key] : acc; - }, []); - return apps; - }; - try { - const timer = createTimer(); - await queueEvent(timer); - const doc = (PHANTOM_DARKNESS && - PHANTOM_DARKNESS.document && - PHANTOM_DARKNESS.document.body ? PHANTOM_DARKNESS.document : - document); - const id = `font-fingerprint`; - const div = doc.createElement('div'); - div.setAttribute('id', id); - doc.body.appendChild(div); - const { emojiSet, pixelSizeSystemSum, } = getPixelEmojis({ - doc, - id, - emojis: EMOJIS, - }) || {}; - const fontList = FONT_LIST; - const fontFaceLoadFonts = await getFontFaceLoadFonts(fontList); - const platformVersion = getPlatformVersion(fontFaceLoadFonts); - const apps = getDesktopApps(fontFaceLoadFonts); - // detect lies - const lied = (lieProps['FontFace.load'] || - lieProps['FontFace.family'] || - lieProps['FontFace.status'] || - lieProps['String.fromCodePoint'] || - lieProps['CSSStyleDeclaration.setProperty'] || - lieProps['CSS2Properties.setProperty']) || false; - if (isFontOSBad(USER_AGENT_OS, fontFaceLoadFonts)) { - LowerEntropy.FONTS = true, - Analysis.FontOsIsBad = true; - sendToTrash('platform', `${USER_AGENT_OS} system and fonts are uncommon`); - } - logTestResult({ time: timer.stop(), test: 'fonts', passed: true }); - return { - fontFaceLoadFonts, - platformVersion, - apps, - emojiSet, - pixelSizeSystemSum, - lied, - }; - } - catch (error) { - logTestResult({ test: 'fonts', passed: false }); - captureError(error); - return; - } - } - function fontsHTML(fp) { - if (!fp.fonts) { - return ` -
- Fonts -
load (0):
-
apps:${HTMLNote.BLOCKED}
-
${HTMLNote.BLOCKED}
-
${HTMLNote.BLOCKED}
-
`; - } - const { fonts: { $hash, fontFaceLoadFonts, platformVersion, apps, emojiSet, pixelSizeSystemSum, lied, }, } = fp; - const icon = { - 'Linux': '', - 'Apple': '', - 'Windows': '', - 'Android': '', - 'CrOS': '', - }; - const blockHelpTitle = `FontFace.load()\nCSSStyleDeclaration.setProperty()\nblock-size\ninline-size\nhash: ${hashMini(emojiSet)}\n${(emojiSet || []).map((x, i) => i && (i % 6 == 0) ? `${x}\n` : x).join('')}`; - return ` -
- ${performanceLogger.getLog().fonts} - Fonts${hashSlice($hash)} -
load (${fontFaceLoadFonts ? count(fontFaceLoadFonts) : '0'}/${'' + FONT_LIST.length}): ${`Like ${platformVersion}` || ((fonts) => { - return !(fonts || []).length ? '' : ((('' + fonts).match(/Lucida Console/) || []).length ? `${icon.Windows}Like Windows` : - (('' + fonts).match(/Droid Sans Mono|Noto Color Emoji|Roboto/g) || []).length == 3 ? `${icon.Linux}${icon.Android}Like Linux Android` : - (('' + fonts).match(/Droid Sans Mono|Roboto/g) || []).length == 2 ? `${icon.Android}Like Android` : - (('' + fonts).match(/Noto Color Emoji|Roboto/g) || []).length == 2 ? `${icon.CrOS}Like Chrome OS` : - (('' + fonts).match(/Noto Color Emoji/) || []).length ? `${icon.Linux}Like Linux` : - (('' + fonts).match(/Arimo/) || []).length ? `${icon.Linux}Like Linux` : - (('' + fonts).match(/Helvetica Neue/g) || []).length == 2 ? `${icon.Apple}Like Apple` : - `${(fonts || [])[0]}...`); - })(fontFaceLoadFonts)}
-
apps: ${(apps || []).length ? apps.join(', ') : HTMLNote.UNSUPPORTED}
-
- ${fontFaceLoadFonts.join(', ') || HTMLNote.UNSUPPORTED} -
-
-
-
${pixelSizeSystemSum || HTMLNote.UNSUPPORTED} -
${formatEmojiSet(emojiSet)} -
-
-
- `; - } - - function getPlatformEstimate() { - if (!IS_BLINK) - return []; - const v80 = 'getVideoPlaybackQuality' in HTMLVideoElement.prototype; - const v81 = CSS.supports('color-scheme: initial'); - const v84 = CSS.supports('appearance: initial'); - const v86 = 'DisplayNames' in Intl; - const v88 = CSS.supports('aspect-ratio: initial'); - const v89 = CSS.supports('border-end-end-radius: initial'); - const v95 = 'randomUUID' in Crypto.prototype; - const hasBarcodeDetector = 'BarcodeDetector' in window; - // @ts-expect-error if not supported - const hasDownlinkMax = 'downlinkMax' in (window.NetworkInformation?.prototype || {}); - const hasContentIndex = 'ContentIndex' in window; - const hasContactsManager = 'ContactsManager' in window; - const hasEyeDropper = 'EyeDropper' in window; - const hasFileSystemWritableFileStream = 'FileSystemWritableFileStream' in window; - const hasHid = 'HID' in window && 'HIDDevice' in window; - const hasSerialPort = 'SerialPort' in window && 'Serial' in window; - const hasSharedWorker = 'SharedWorker' in window; - const hasTouch = 'ontouchstart' in Window && 'TouchEvent' in window; - const hasAppBadge = 'setAppBadge' in Navigator.prototype; - const hasFeature = (version, condition) => { - return (version ? [condition] : []); - }; - const estimate = { - ["Android" /* Platform.ANDROID */]: [ - ...hasFeature(v88, hasBarcodeDetector), - ...hasFeature(v84, hasContentIndex), - ...hasFeature(v80, hasContactsManager), - hasDownlinkMax, - ...hasFeature(v95, !hasEyeDropper), - ...hasFeature(v86, !hasFileSystemWritableFileStream), - ...hasFeature(v89, !hasHid), - ...hasFeature(v89, !hasSerialPort), - !hasSharedWorker, - hasTouch, - ...hasFeature(v81, !hasAppBadge), - ], - ["Chrome OS" /* Platform.CHROME_OS */]: [ - ...hasFeature(v88, hasBarcodeDetector), - ...hasFeature(v84, !hasContentIndex), - ...hasFeature(v80, !hasContactsManager), - hasDownlinkMax, - ...hasFeature(v95, hasEyeDropper), - ...hasFeature(v86, hasFileSystemWritableFileStream), - ...hasFeature(v89, hasHid), - ...hasFeature(v89, hasSerialPort), - hasSharedWorker, - hasTouch || !hasTouch, - ...hasFeature(v81, !hasAppBadge), - ], - ["Windows" /* Platform.WINDOWS */]: [ - ...hasFeature(v88, !hasBarcodeDetector), - ...hasFeature(v84, !hasContentIndex), - ...hasFeature(v80, !hasContactsManager), - !hasDownlinkMax, - ...hasFeature(v95, hasEyeDropper), - ...hasFeature(v86, hasFileSystemWritableFileStream), - ...hasFeature(v89, hasHid), - ...hasFeature(v89, hasSerialPort), - hasSharedWorker, - hasTouch || !hasTouch, - ...hasFeature(v81, hasAppBadge), - ], - ["Mac" /* Platform.MAC */]: [ - ...hasFeature(v88, hasBarcodeDetector), - ...hasFeature(v84, !hasContentIndex), - ...hasFeature(v80, !hasContactsManager), - !hasDownlinkMax, - ...hasFeature(v95, hasEyeDropper), - ...hasFeature(v86, hasFileSystemWritableFileStream), - ...hasFeature(v89, hasHid), - ...hasFeature(v89, hasSerialPort), - hasSharedWorker, - !hasTouch, - ...hasFeature(v81, hasAppBadge), - ], - ["Linux" /* Platform.LINUX */]: [ - ...hasFeature(v88, !hasBarcodeDetector), - ...hasFeature(v84, !hasContentIndex), - ...hasFeature(v80, !hasContactsManager), - !hasDownlinkMax, - ...hasFeature(v95, hasEyeDropper), - ...hasFeature(v86, hasFileSystemWritableFileStream), - ...hasFeature(v89, hasHid), - ...hasFeature(v89, hasSerialPort), - hasSharedWorker, - !hasTouch || !hasTouch, - ...hasFeature(v81, !hasAppBadge), - ], - }; - // Chrome only features - const headlessEstimate = { - noContentIndex: v84 && !hasContentIndex, - noContactsManager: v80 && !hasContactsManager, - noDownlinkMax: !hasDownlinkMax, - }; - const scores = Object.keys(estimate).reduce((acc, key) => { - const list = estimate[key]; - const score = +((list.filter((x) => x).length / list.length).toFixed(2)); - acc[key] = score; - return acc; - }, {}); - const platform = Object.keys(scores).reduce((a, b) => scores[a] > scores[b] ? a : b); - const highestScore = scores[platform]; - return [scores, highestScore, headlessEstimate]; - } - - const SYSTEM_FONTS = [ - 'caption', - 'icon', - 'menu', - 'message-box', - 'small-caption', - 'status-bar', - ]; - - const GeckoFonts = { - '-apple-system': "Mac" /* Platform.MAC */, - 'Segoe UI': "Windows" /* Platform.WINDOWS */, - 'Tahoma': "Windows" /* Platform.WINDOWS */, - 'Yu Gothic UI': "Windows" /* Platform.WINDOWS */, - 'Microsoft JhengHei UI': "Windows" /* Platform.WINDOWS */, - 'Microsoft YaHei UI': "Windows" /* Platform.WINDOWS */, - 'Meiryo UI': "Windows" /* Platform.WINDOWS */, - 'Cantarell': "Linux" /* Platform.LINUX */, - 'Ubuntu': "Linux" /* Platform.LINUX */, - 'Sans': "Linux" /* Platform.LINUX */, - 'sans-serif': "Linux" /* Platform.LINUX */, - 'Fira Sans': "Linux" /* Platform.LINUX */, - 'Roboto': "Android" /* Platform.ANDROID */, - }; - function getSystemFonts() { - const { body } = document; - const el = document.createElement('div'); - body.appendChild(el); - try { - const systemFonts = String([ - ...SYSTEM_FONTS.reduce((acc, font) => { - el.setAttribute('style', `font: ${font} !important`); - return acc.add(getComputedStyle(el).fontFamily); - }, new Set()), - ]); - const geckoPlatform = GeckoFonts[systemFonts]; - return GeckoFonts[systemFonts] ? `${systemFonts}:${geckoPlatform}` : systemFonts; - } - catch (err) { - return ''; - } - finally { - body.removeChild(el); - } - } - - /* eslint-disable new-cap */ - async function getHeadlessFeatures({ webgl, workerScope, }) { - try { - const timer = createTimer(); - await queueEvent(timer); - const mimeTypes = Object.keys({ ...navigator.mimeTypes }); - const systemFonts = getSystemFonts(); - const [scores, highestScore, headlessEstimate] = getPlatformEstimate(); - const data = { - chromium: IS_BLINK, - likeHeadless: { - noChrome: IS_BLINK && !('chrome' in window), - hasPermissionsBug: (IS_BLINK && - 'permissions' in navigator && - await (async () => { - const res = await navigator.permissions.query({ name: 'notifications' }); - return (res.state == 'prompt' && - 'Notification' in window && - Notification.permission === 'denied'); - })()), - noPlugins: IS_BLINK && navigator.plugins.length === 0, - noMimeTypes: IS_BLINK && mimeTypes.length === 0, - notificationIsDenied: (IS_BLINK && - 'Notification' in window && - (Notification.permission == 'denied')), - hasKnownBgColor: IS_BLINK && (() => { - let rendered = PARENT_PHANTOM; - if (!PARENT_PHANTOM) { - rendered = document.createElement('div'); - document.body.appendChild(rendered); - } - if (!rendered) - return false; - rendered.setAttribute('style', `background-color: ActiveText`); - const { backgroundColor: activeText } = getComputedStyle(rendered) || []; - if (!PARENT_PHANTOM) { - document.body.removeChild(rendered); - } - return activeText === 'rgb(255, 0, 0)'; - })(), - prefersLightColor: matchMedia('(prefers-color-scheme: light)').matches, - uaDataIsBlank: ('userAgentData' in navigator && ( - // @ts-expect-error if userAgentData is null - navigator.userAgentData?.platform === '' || - // @ts-expect-error if userAgentData is null - await navigator.userAgentData.getHighEntropyValues(['platform']).platform === '')), - pdfIsDisabled: ('pdfViewerEnabled' in navigator && navigator.pdfViewerEnabled === false), - noTaskbar: (screen.height === screen.availHeight && - screen.width === screen.availWidth), - hasVvpScreenRes: ((innerWidth === screen.width && outerHeight === screen.height) || ('visualViewport' in window && - // @ts-expect-error if unsupported - (visualViewport.width === screen.width && visualViewport.height === screen.height))), - hasSwiftShader: /SwiftShader/.test(workerScope?.webglRenderer), - noWebShare: IS_BLINK && CSS.supports('accent-color: initial') && (!('share' in navigator) || !('canShare' in navigator)), - noContentIndex: !!headlessEstimate?.noContentIndex, - noContactsManager: !!headlessEstimate?.noContactsManager, - noDownlinkMax: !!headlessEstimate?.noDownlinkMax, - }, - headless: { - webDriverIsOn: ((CSS.supports('border-end-end-radius: initial') && navigator.webdriver === undefined) || - !!navigator.webdriver || - !!lieProps['Navigator.webdriver']), - hasHeadlessUA: (/HeadlessChrome/.test(navigator.userAgent) || - /HeadlessChrome/.test(navigator.appVersion)), - hasHeadlessWorkerUA: !!workerScope && (/HeadlessChrome/.test(workerScope.userAgent)), - }, - stealth: { - hasIframeProxy: (() => { - try { - const iframe = document.createElement('iframe'); - iframe.srcdoc = instanceId; - return !!iframe.contentWindow; - } - catch (err) { - return true; - } - })(), - hasHighChromeIndex: (() => { - const key = 'chrome'; - const highIndexRange = -50; - return (Object.keys(window).slice(highIndexRange).includes(key) && - Object.getOwnPropertyNames(window).slice(highIndexRange).includes(key)); - })(), - hasBadChromeRuntime: (() => { - // @ts-expect-error if unsupported - if (!('chrome' in window && 'runtime' in chrome)) { - return false; - } - try { - // @ts-expect-error if unsupported - if ('prototype' in chrome.runtime.sendMessage || - // @ts-expect-error if unsupported - 'prototype' in chrome.runtime.connect) { - return true; - } - // @ts-expect-error if unsupported - new chrome.runtime.sendMessage; - // @ts-expect-error if unsupported - new chrome.runtime.connect; - return true; - } - catch (err) { - return err.constructor.name != 'TypeError' ? true : false; - } - })(), - hasToStringProxy: (!!lieProps['Function.toString']), - hasBadWebGL: (() => { - const { UNMASKED_RENDERER_WEBGL: gpu } = webgl?.parameters || {}; - const { webglRenderer: workerGPU } = workerScope || {}; - return (gpu && workerGPU && (gpu !== workerGPU)); - })(), - }, - }; - const { likeHeadless, headless, stealth } = data; - const likeHeadlessKeys = Object.keys(likeHeadless); - const headlessKeys = Object.keys(headless); - const stealthKeys = Object.keys(stealth); - const likeHeadlessRating = +((likeHeadlessKeys.filter((key) => likeHeadless[key]).length / likeHeadlessKeys.length) * 100).toFixed(0); - const headlessRating = +((headlessKeys.filter((key) => headless[key]).length / headlessKeys.length) * 100).toFixed(0); - const stealthRating = +((stealthKeys.filter((key) => stealth[key]).length / stealthKeys.length) * 100).toFixed(0); - logTestResult({ time: timer.stop(), test: 'headless', passed: true }); - return { - ...data, - likeHeadlessRating, - headlessRating, - stealthRating, - systemFonts, - platformEstimate: [scores, highestScore], - }; - } - catch (error) { - logTestResult({ test: 'headless', passed: false }); - captureError(error); - return; - } - } - function headlessFeaturesHTML(fp) { - if (!fp.headless) { - return ` -
- Headless -
chromium: ${HTMLNote.BLOCKED}
-
0% like headless: ${HTMLNote.BLOCKED}
-
0% headless: ${HTMLNote.BLOCKED}
-
0% stealth: ${HTMLNote.BLOCKED}
-
platform hints:
-
${HTMLNote.BLOCKED}
-
`; - } - const { headless: data, } = fp; - const { $hash, chromium, likeHeadless, likeHeadlessRating, headless, headlessRating, stealth, stealthRating, systemFonts, platformEstimate, } = data || {}; - const [scores, highestScore] = platformEstimate || []; - const IconMap = { - ["Android" /* Platform.ANDROID */]: ``, - ["Chrome OS" /* Platform.CHROME_OS */]: ``, - ["Windows" /* Platform.WINDOWS */]: ``, - ["Mac" /* Platform.MAC */]: ``, - ["Linux" /* Platform.LINUX */]: ``, - }; - const scoreKeys = Object.keys(scores || {}); - const platformTemplate = !scores ? '' : ` - ${scoreKeys.map((key) => (scores[key] * 100).toFixed(0)).join(':')} -
${scoreKeys.map((key) => { - const score = scores[key]; - const style = ` - filter: opacity(${score == highestScore ? 100 : 15}%); - `; - return `${IconMap[key]}`; - }).join('')} - `; - return ` -
- - ${performanceLogger.getLog().headless} - Headless${hashSlice($hash)} -
chromium: ${'' + chromium}
- -
${'' + headlessRating}% headless: ${modal('creep-headless', 'Headless

' + - Object.keys(headless).map((key) => `${key}: ${'' + headless[key]}`).join('
'), hashMini(headless))}
-
${'' + stealthRating}% stealth: ${modal('creep-stealth', 'Stealth

' + - Object.keys(stealth).map((key) => `${key}: ${'' + stealth[key]}`).join('
'), hashMini(stealth))}
-
platform hints:
-
- ${systemFonts ? `
${systemFonts}
` : ''} - ${platformTemplate ? `
${platformTemplate}
` : ''} -
-
`; - } - - async function getIntl() { - const getLocale = (intl) => { - const constructors = [ - 'Collator', - 'DateTimeFormat', - 'DisplayNames', - 'ListFormat', - 'NumberFormat', - 'PluralRules', - 'RelativeTimeFormat', - ]; - // @ts-ignore - const locale = constructors.reduce((acc, name) => { - try { - const obj = new intl[name]; - if (!obj) { - return acc; - } - const { locale } = obj.resolvedOptions() || {}; - return [...acc, locale]; - } - catch (error) { - return acc; - } - }, []); - return [...new Set(locale)]; - }; - try { - const timer = createTimer(); - await queueEvent(timer); - const lied = (lieProps['Intl.Collator.resolvedOptions'] || - lieProps['Intl.DateTimeFormat.resolvedOptions'] || - lieProps['Intl.DisplayNames.resolvedOptions'] || - lieProps['Intl.ListFormat.resolvedOptions'] || - lieProps['Intl.NumberFormat.resolvedOptions'] || - lieProps['Intl.PluralRules.resolvedOptions'] || - lieProps['Intl.RelativeTimeFormat.resolvedOptions']) || false; - const dateTimeFormat = caniuse(() => { - return new Intl.DateTimeFormat(undefined, { - month: 'long', - timeZoneName: 'long', - }).format(963644400000); - }); - const displayNames = caniuse(() => { - return new Intl.DisplayNames(undefined, { - type: 'language', - }).of('en-US'); - }); - const listFormat = caniuse(() => { - // @ts-ignore - return new Intl.ListFormat(undefined, { - style: 'long', - type: 'disjunction', - }).format(['0', '1']); - }); - const numberFormat = caniuse(() => { - return new Intl.NumberFormat(undefined, { - notation: 'compact', - compactDisplay: 'long', - }).format(21000000); - }); - const pluralRules = caniuse(() => { - return new Intl.PluralRules().select(1); - }); - const relativeTimeFormat = caniuse(() => { - return new Intl.RelativeTimeFormat(undefined, { - localeMatcher: 'best fit', - numeric: 'auto', - style: 'long', - }).format(1, 'year'); - }); - const locale = getLocale(Intl); - logTestResult({ time: timer.stop(), test: 'intl', passed: true }); - return { - dateTimeFormat, - displayNames, - listFormat, - numberFormat, - pluralRules, - relativeTimeFormat, - locale: '' + locale, - lied, - }; - } - catch (error) { - logTestResult({ test: 'intl', passed: false }); - captureError(error); - return; - } - } - function intlHTML(fp) { - if (!fp.htmlElementVersion) { - return ` -
- Intl -
locale: ${HTMLNote.Blocked}
-
date: ${HTMLNote.Blocked}
-
display: ${HTMLNote.Blocked}
-
list: ${HTMLNote.Blocked}
-
number: ${HTMLNote.Blocked}
-
plural: ${HTMLNote.Blocked}
-
relative: ${HTMLNote.Blocked}
-
`; - } - const { $hash, dateTimeFormat, displayNames, listFormat, numberFormat, pluralRules, relativeTimeFormat, locale, lied, } = fp.intl || {}; - return ` -
- ${performanceLogger.getLog().intl} - Intl${hashSlice($hash)} -
- ${[ - locale, - dateTimeFormat, - displayNames, - numberFormat, - relativeTimeFormat, - listFormat, - pluralRules, - ].join('
')} -
-
- `; - } - - function getMaths() { - try { - const timer = createTimer(); - timer.start(); - // detect failed math equality lie - const check = [ - 'acos', - 'acosh', - 'asin', - 'asinh', - 'atan', - 'atanh', - 'atan2', - 'cbrt', - 'cos', - 'cosh', - 'expm1', - 'exp', - 'hypot', - 'log', - 'log1p', - 'log10', - 'sin', - 'sinh', - 'sqrt', - 'tan', - 'tanh', - 'pow', - ]; - let lied = false; - check.forEach((prop) => { - if (!!lieProps[`Math.${prop}`]) { - lied = true; - } - const test = (prop == 'cos' ? [1e308] : - prop == 'acos' || prop == 'asin' || prop == 'atanh' ? [0.5] : - prop == 'pow' || prop == 'atan2' ? [Math.PI, 2] : - [Math.PI]); - const res1 = Math[prop](...test); - const res2 = Math[prop](...test); - const matching = isNaN(res1) && isNaN(res2) ? true : res1 == res2; - if (!matching) { - lied = true; - const mathLie = `expected x and got y`; - documentLie(`Math.${prop}`, mathLie); - } - return; - }); - const n = 0.123; - const bigN = 5.860847362277284e+38; - const fns = [ - ['acos', [n], `acos(${n})`, 1.4474840516030247, NaN, NaN, 1.4474840516030245], - ['acos', [Math.SQRT1_2], 'acos(Math.SQRT1_2)', 0.7853981633974483, NaN, NaN, NaN], - ['acosh', [1e308], 'acosh(1e308)', 709.889355822726, NaN, NaN, NaN], - ['acosh', [Math.PI], 'acosh(Math.PI)', 1.811526272460853, NaN, NaN, NaN], - ['acosh', [Math.SQRT2], 'acosh(Math.SQRT2)', 0.881373587019543, NaN, NaN, 0.8813735870195432], - ['asin', [n], `asin(${n})`, 0.12331227519187199, NaN, NaN, NaN], - ['asinh', [1e300], 'asinh(1e308)', 691.4686750787736, NaN, NaN, NaN], - ['asinh', [Math.PI], 'asinh(Math.PI)', 1.8622957433108482, NaN, NaN, NaN], - ['atan', [2], 'atan(2)', 1.1071487177940904, NaN, NaN, 1.1071487177940906], - ['atan', [Math.PI], 'atan(Math.PI)', 1.2626272556789115, NaN, NaN, NaN], - ['atanh', [0.5], 'atanh(0.5)', 0.5493061443340548, NaN, NaN, 0.5493061443340549], - ['atan2', [1e-310, 2], 'atan2(1e-310, 2)', 5e-311, NaN, NaN, NaN], - ['atan2', [Math.PI, 2], 'atan2(Math.PI)', 1.0038848218538872, NaN, NaN, NaN], - ['cbrt', [100], 'cbrt(100)', 4.641588833612779, NaN, NaN, NaN], - ['cbrt', [Math.PI], 'cbrt(Math.PI)', 1.4645918875615231, NaN, NaN, 1.4645918875615234], - ['cos', [n], `cos(${n})`, 0.9924450321351935, NaN, NaN, NaN], - ['cos', [Math.PI], 'cos(Math.PI)', -1, NaN, NaN, NaN], - ['cos', [bigN], `cos(${bigN})`, -0.10868049424995659, NaN, -0.9779661551196617, NaN], - ['cos', [-1e308], 'cos(-1e308)', -0.8913089376870335, NaN, 0.99970162388838, NaN], - ['cos', [13 * Math.E], 'cos(13*Math.E)', -0.7108118501064331, -0.7108118501064332, NaN, NaN], - ['cos', [57 * Math.E], 'cos(57*Math.E)', -0.536911695749024, -0.5369116957490239, NaN, NaN], - ['cos', [21 * Math.LN2], 'cos(21*Math.LN2)', -0.4067775970251724, -0.40677759702517235, -0.6534063185820197, NaN], - ['cos', [51 * Math.LN2], 'cos(51*Math.LN2)', -0.7017203400855446, -0.7017203400855445, NaN, NaN], - ['cos', [21 * Math.LOG2E], 'cos(21*Math.LOG2E)', 0.4362848063618998, 0.43628480636189976, NaN, NaN], - ['cos', [25 * Math.SQRT2], 'cos(25*Math.SQRT2)', -0.6982689820462377, -0.6982689820462376, NaN, NaN], - ['cos', [50 * Math.SQRT1_2], 'cos(50*Math.SQRT1_2)', -0.6982689820462377, -0.6982689820462376, NaN, NaN], - ['cos', [21 * Math.SQRT1_2], 'cos(21*Math.SQRT1_2)', -0.6534063185820198, NaN, NaN, NaN], - ['cos', [17 * Math.LOG10E], 'cos(17*Math.LOG10E)', 0.4537557425982784, 0.45375574259827833, NaN, NaN], - ['cos', [2 * Math.LOG10E], 'cos(2*Math.LOG10E)', 0.6459044007438142, NaN, 0.6459044007438141, NaN], - ['cosh', [1], 'cosh(1)', 1.5430806348152437, NaN, NaN, NaN], - ['cosh', [Math.PI], 'cosh(Math.PI)', 11.591953275521519, NaN, NaN, NaN], - ['cosh', [492 * Math.LOG2E], 'cosh(492*Math.LOG2E)', 9.199870313877772e+307, 9.199870313877774e+307, NaN, NaN], - ['cosh', [502 * Math.SQRT2], 'cosh(502*Math.SQRT2)', 1.0469199669023138e+308, 1.046919966902314e+308, NaN, NaN], - ['expm1', [1], 'expm1(1)', 1.718281828459045, NaN, NaN, 1.7182818284590453], - ['expm1', [Math.PI], 'expm1(Math.PI)', 22.140692632779267, NaN, NaN, NaN], - ['exp', [n], `exp(${n})`, 1.1308844209474893, NaN, NaN, NaN], - ['exp', [Math.PI], 'exp(Math.PI)', 23.140692632779267, NaN, NaN, NaN], - ['hypot', [1, 2, 3, 4, 5, 6], 'hypot(1, 2, 3, 4, 5, 6)', 9.539392014169456, NaN, NaN, NaN], - ['hypot', [bigN, bigN], `hypot(${bigN}, ${bigN})`, 8.288489826731116e+38, 8.288489826731114e+38, NaN, NaN], - ['hypot', [2 * Math.E, -100], 'hypot(2*Math.E, -100)', 100.14767208675259, 100.14767208675258, NaN, NaN], - ['hypot', [6 * Math.PI, -100], 'hypot(6*Math.PI, -100)', 101.76102278593319, 101.7610227859332, NaN, NaN], - ['hypot', [2 * Math.LN2, -100], 'hypot(2*Math.LN2, -100)', 100.0096085986525, 100.00960859865252, NaN, NaN], - ['hypot', [Math.LOG2E, -100], 'hypot(Math.LOG2E, -100)', 100.01040630344929, 100.01040630344927, NaN, NaN], - ['hypot', [Math.SQRT2, -100], 'hypot(Math.SQRT2, -100)', 100.00999950004999, 100.00999950005, NaN, NaN], - ['hypot', [Math.SQRT1_2, -100], 'hypot(Math.SQRT1_2, -100)', 100.0024999687508, 100.00249996875078, NaN, NaN], - ['hypot', [2 * Math.LOG10E, -100], 'hypot(2*Math.LOG10E, -100)', 100.00377216279416, 100.00377216279418, NaN, NaN], - ['log', [n], `log(${n})`, -2.0955709236097197, NaN, NaN, NaN], - ['log', [Math.PI], 'log(Math.PI)', 1.1447298858494002, NaN, NaN, NaN], - ['log1p', [n], `log1p(${n})`, 0.11600367575630613, NaN, NaN, NaN], - ['log1p', [Math.PI], 'log1p(Math.PI)', 1.4210804127942926, NaN, NaN, NaN], - ['log10', [n], `log10(${n})`, -0.9100948885606021, NaN, NaN, NaN], - ['log10', [Math.PI], 'log10(Math.PI)', 0.4971498726941338, 0.49714987269413385, NaN, NaN], - ['log10', [Math.E], 'log10(Math.E)', 0.4342944819032518, NaN, NaN, NaN], - ['log10', [34 * Math.E], 'log10(34*Math.E)', 1.9657733989455068, 1.965773398945507, NaN, NaN], - ['log10', [Math.LN2], 'log10(Math.LN2)', -0.1591745389548616, NaN, NaN, NaN], - ['log10', [11 * Math.LN2], 'log10(11*Math.LN2)', 0.8822181462033634, 0.8822181462033635, NaN, NaN], - ['log10', [Math.LOG2E], 'log10(Math.LOG2E)', 0.15917453895486158, NaN, NaN, NaN], - ['log10', [43 * Math.LOG2E], 'log10(43*Math.LOG2E)', 1.792642994534448, 1.7926429945344482, NaN, NaN], - ['log10', [Math.LOG10E], 'log10(Math.LOG10E)', -0.36221568869946325, NaN, NaN, NaN], - ['log10', [7 * Math.LOG10E], 'log10(7*Math.LOG10E)', 0.4828823513147936, 0.48288235131479357, NaN, NaN], - ['log10', [Math.SQRT1_2], 'log10(Math.SQRT1_2)', -0.15051499783199057, NaN, NaN, NaN], - ['log10', [2 * Math.SQRT1_2], 'log10(2*Math.SQRT1_2)', 0.1505149978319906, 0.15051499783199063, NaN, NaN], - ['log10', [Math.SQRT2], 'log10(Math.SQRT2)', 0.1505149978319906, 0.15051499783199063, NaN, NaN], - ['sin', [bigN], `sin(${bigN})`, 0.994076732536068, NaN, -0.20876350121720488, NaN], - ['sin', [Math.PI], 'sin(Math.PI)', 1.2246467991473532e-16, NaN, 1.2246063538223773e-16, NaN], - ['sin', [39 * Math.E], 'sin(39*Math.E)', -0.7181630308570677, -0.7181630308570678, NaN, NaN], - ['sin', [35 * Math.LN2], 'sin(35*Math.LN2)', -0.7659964138980511, -0.765996413898051, NaN, NaN], - ['sin', [110 * Math.LOG2E], 'sin(110*Math.LOG2E)', 0.9989410140273756, 0.9989410140273757, NaN, NaN], - ['sin', [7 * Math.LOG10E], 'sin(7*Math.LOG10E)', 0.10135692924965616, 0.10135692924965614, NaN, NaN], - ['sin', [35 * Math.SQRT1_2], 'sin(35*Math.SQRT1_2)', -0.3746357547858202, -0.37463575478582023, NaN, NaN], - ['sin', [21 * Math.SQRT2], 'sin(21*Math.SQRT2)', -0.9892668187780498, -0.9892668187780497, NaN, NaN], - ['sinh', [1], 'sinh(1)', 1.1752011936438014, NaN, NaN, NaN], - ['sinh', [Math.PI], 'sinh(Math.PI)', 11.548739357257748, NaN, NaN, 11.548739357257746], - ['sinh', [Math.E], 'sinh(Math.E)', 7.544137102816975, NaN, NaN, NaN], - ['sinh', [Math.LN2], 'sinh(Math.LN2)', 0.75, NaN, NaN, NaN], - ['sinh', [Math.LOG2E], 'sinh(Math.LOG2E)', 1.9978980091062795, NaN, NaN, NaN], - ['sinh', [492 * Math.LOG2E], 'sinh(492*Math.LOG2E)', 9.199870313877772e+307, 9.199870313877774e+307, NaN, NaN], - ['sinh', [Math.LOG10E], 'sinh(Math.LOG10E)', 0.44807597941469024, NaN, NaN, NaN], - ['sinh', [Math.SQRT1_2], 'sinh(Math.SQRT1_2)', 0.7675231451261164, NaN, NaN, NaN], - ['sinh', [Math.SQRT2], 'sinh(Math.SQRT2)', 1.935066822174357, NaN, NaN, 1.9350668221743568], - ['sinh', [502 * Math.SQRT2], 'sinh(502*Math.SQRT2)', 1.0469199669023138e+308, 1.046919966902314e+308, NaN, NaN], - ['sqrt', [n], `sqrt(${n})`, 0.3507135583350036, NaN, NaN, NaN], - ['sqrt', [Math.PI], 'sqrt(Math.PI)', 1.7724538509055159, NaN, NaN, NaN], - ['tan', [-1e308], 'tan(-1e308)', 0.5086861259107568, NaN, NaN, 0.5086861259107567], - ['tan', [Math.PI], 'tan(Math.PI)', -1.2246467991473532e-16, NaN, NaN, NaN], - ['tan', [6 * Math.E], 'tan(6*Math.E)', 0.6866761546452431, 0.686676154645243, NaN, NaN], - ['tan', [6 * Math.LN2], 'tan(6*Math.LN2)', 1.6182817135715877, 1.618281713571588, NaN, 1.6182817135715875], - ['tan', [10 * Math.LOG2E], 'tan(10*Math.LOG2E)', -3.3537128705376014, -3.353712870537601, NaN, -3.353712870537602], - ['tan', [17 * Math.SQRT2], 'tan(17*Math.SQRT2)', -1.9222955461799982, -1.922295546179998, NaN, NaN], - ['tan', [34 * Math.SQRT1_2], 'tan(34*Math.SQRT1_2)', -1.9222955461799982, -1.922295546179998, NaN, NaN], - ['tan', [10 * Math.LOG10E], 'tan(10*Math.LOG10E)', 2.5824856130712432, 2.5824856130712437, NaN, NaN], - ['tanh', [n], `tanh(${n})`, 0.12238344189440875, NaN, NaN, 0.12238344189440876], - ['tanh', [Math.PI], 'tanh(Math.PI)', 0.99627207622075, NaN, NaN, NaN], - ['pow', [n, -100], `pow(${n}, -100)`, 1.022089333584519e+91, 1.0220893335845176e+91, NaN, NaN], - ['pow', [Math.PI, -100], 'pow(Math.PI, -100)', 1.9275814160560204e-50, 1.9275814160560185e-50, NaN, 1.9275814160560206e-50], - ['pow', [Math.E, -100], 'pow(Math.E, -100)', 3.7200759760208555e-44, 3.720075976020851e-44, NaN, NaN], - ['pow', [Math.LN2, -100], 'pow(Math.LN2, -100)', 8269017203802394, 8269017203802410, NaN, NaN], - ['pow', [Math.LN10, -100], 'pow(Math.LN10, -100)', 6.003867926738829e-37, 6.003867926738811e-37, NaN, NaN], - ['pow', [Math.LOG2E, -100], 'pow(Math.LOG2E, -100)', 1.20933355845501e-16, 1.2093335584550061e-16, NaN, NaN], - ['pow', [Math.LOG10E, -100], 'pow(Math.LOG10E, -100)', 1.6655929347585958e+36, 1.665592934758592e+36, NaN, 1.6655929347585955e+36], - ['pow', [Math.SQRT1_2, -100], 'pow(Math.SQRT1_2, -100)', 1125899906842616.2, 1125899906842611.5, NaN, NaN], - ['pow', [Math.SQRT2, -100], 'pow(Math.SQRT2, -100)', 8.881784197001191e-16, 8.881784197001154e-16, NaN, NaN], - ['polyfill', [2e-3 ** -100], 'polyfill pow(2e-3, -100)', 7.888609052210102e+269, 7.888609052210126e+269, NaN, NaN], - ]; - const data = {}; - fns.forEach((fn) => { - data[fn[2]] = attempt(() => { - // @ts-ignore - const result = fn[0] != 'polyfill' ? Math[fn[0]](...fn[1]) : fn[1]; - const chrome = result == fn[3]; - const firefox = fn[4] ? result == fn[4] : false; - const torBrowser = fn[5] ? result == fn[5] : false; - const safari = fn[6] ? result == fn[6] : false; - return { result, chrome, firefox, torBrowser, safari }; - }); - }); - logTestResult({ time: timer.stop(), test: 'math', passed: true }); - return { data, lied }; - } - catch (error) { - logTestResult({ test: 'math', passed: false }); - captureError(error); - return; - } - } - function mathsHTML(fp) { - if (!fp.maths) { - return ` -
- Math -
results: ${HTMLNote.Blocked}
-
-
${HTMLNote.Blocked}
-
- -
`; - } - const { maths: { data, $hash, lied, }, } = fp; - const header = ` - -
-
C - Chromium -
F - Firefox -
T - Tor Browser -
S - Safari -
`; - const results = Object.keys(data).map((key) => { - const value = data[key]; - const { chrome, firefox, torBrowser, safari } = value; - return ` - ${chrome ? 'C' : '-'}${firefox ? 'F' : '-'}${torBrowser ? 'T' : '-'}${safari ? 'S' : '-'} ${key}`; - }); - return ` -
- ${performanceLogger.getLog().math} - Math${hashSlice($hash)} -
results: ${!data ? HTMLNote.Blocked : - modal('creep-maths', header + results.join('
'))}
-
- `; - } - - // inspired by - // - https://privacycheck.sec.lrz.de/active/fp_cpt/fp_can_play_type.html - // - https://arkenfox.github.io/TZP - const getMimeTypeShortList = () => [ - 'audio/ogg; codecs="vorbis"', - 'audio/mpeg', - 'audio/mpegurl', - 'audio/wav; codecs="1"', - 'audio/x-m4a', - 'audio/aac', - 'video/ogg; codecs="theora"', - 'video/quicktime', - 'video/mp4; codecs="avc1.42E01E"', - 'video/webm; codecs="vp8"', - 'video/webm; codecs="vp9"', - 'video/x-matroska', - ].sort(); - async function getMedia() { - const getMimeTypes = () => { - try { - const mimeTypes = getMimeTypeShortList(); - const videoEl = document.createElement('video'); - const audioEl = new Audio(); - const isMediaRecorderSupported = 'MediaRecorder' in window; - const types = mimeTypes.reduce((acc, type) => { - const data = { - mimeType: type, - audioPlayType: audioEl.canPlayType(type), - videoPlayType: videoEl.canPlayType(type), - mediaSource: MediaSource.isTypeSupported(type), - mediaRecorder: isMediaRecorderSupported ? MediaRecorder.isTypeSupported(type) : false, - }; - if (!data.audioPlayType && !data.videoPlayType && !data.mediaSource && !data.mediaRecorder) { - return acc; - } - // @ts-ignore - acc.push(data); - return acc; - }, []); - return types; - } - catch (error) { - return; - } - }; - try { - const timer = createTimer(); - timer.start(); - const mimeTypes = getMimeTypes(); - logTestResult({ time: timer.stop(), test: 'media', passed: true }); - return { mimeTypes }; - } - catch (error) { - logTestResult({ test: 'media', passed: false }); - captureError(error); - return; - } - } - function mediaHTML(fp) { - if (!fp.media) { - return ` -
- Media -
mimes (0): ${HTMLNote.BLOCKED}
-
- `; - } - const { media: { mimeTypes, $hash, }, } = fp; - const header = ` - -
-
audioPlayType -
videoPlayType -
mediaSource -
mediaRecorder -
P (Probably) -
M (Maybe) -
T (True) -
- `; - const invalidMimeTypes = !mimeTypes || !mimeTypes.length; - const mimes = invalidMimeTypes ? undefined : mimeTypes.map((type) => { - const { mimeType, audioPlayType, videoPlayType, mediaSource, mediaRecorder } = type; - return ` - ${audioPlayType == 'probably' ? 'P' : audioPlayType == 'maybe' ? 'M' : '-'}${videoPlayType == 'probably' ? 'P' : videoPlayType == 'maybe' ? 'M' : '-'}${mediaSource ? 'T' : '-'}${mediaRecorder ? 'T' : '-'}: ${mimeType} - `; - }); - const mimesListLen = getMimeTypeShortList().length; - return ` -
- ${performanceLogger.getLog().media} - Media${hashSlice($hash)} -
mimes (${count(mimeTypes)}/${mimesListLen}): ${invalidMimeTypes ? HTMLNote.BLOCKED : - modal('creep-media-mimeTypes', header + mimes.join('
'), hashMini(mimeTypes))}
-
- `; - } - - // special thanks to https://arh.antoinevastel.com for inspiration - async function getNavigator(workerScope) { - try { - const timer = createTimer(); - await queueEvent(timer); - let lied = (lieProps['Navigator.appVersion'] || - lieProps['Navigator.deviceMemory'] || - lieProps['Navigator.doNotTrack'] || - lieProps['Navigator.hardwareConcurrency'] || - lieProps['Navigator.language'] || - lieProps['Navigator.languages'] || - lieProps['Navigator.maxTouchPoints'] || - lieProps['Navigator.oscpu'] || - lieProps['Navigator.platform'] || - lieProps['Navigator.userAgent'] || - lieProps['Navigator.vendor'] || - lieProps['Navigator.plugins'] || - lieProps['Navigator.mimeTypes']) || false; - const credibleUserAgent = ('chrome' in window ? navigator.userAgent.includes(navigator.appVersion) : true); - const data = { - platform: attempt(() => { - const { platform } = navigator; - const systems = ['win', 'linux', 'mac', 'arm', 'pike', 'linux', 'iphone', 'ipad', 'ipod', 'android', 'x11']; - const trusted = typeof platform == 'string' && systems.filter((val) => platform.toLowerCase().includes(val))[0]; - if (!trusted) { - sendToTrash(`platform`, `${platform} is unusual`); - } - // user agent os lie - if (USER_AGENT_OS !== PLATFORM_OS) { - lied = true; - documentLie(`Navigator.platform`, `${PLATFORM_OS} platform and ${USER_AGENT_OS} user agent do not match`); - } - if (platform != workerScope.platform) { - lied = true; // documented in the worker source - } - return platform; - }), - system: attempt(() => getOS(navigator.userAgent), 'userAgent system failed'), - userAgentParsed: await attempt(async () => { - const reportedUserAgent = caniuse(() => navigator.userAgent); - const reportedSystem = getOS(reportedUserAgent); - const isBrave = await braveBrowser(); - const report = decryptUserAgent({ - ua: reportedUserAgent, - os: reportedSystem, - isBrave, - }); - return report; - }), - device: attempt(() => getUserAgentPlatform({ userAgent: navigator.userAgent }), 'userAgent device failed'), - userAgent: attempt(() => { - const { userAgent } = navigator; - if (!credibleUserAgent) { - sendToTrash('userAgent', `${userAgent} does not match appVersion`); - } - if (/\s{2,}|^\s|\s$/g.test(userAgent)) { - sendToTrash('userAgent', `extra spaces detected`); - } - const gibbers = gibberish(userAgent); - if (!!gibbers.length) { - sendToTrash(`userAgent is gibberish`, userAgent); - } - if (userAgent != workerScope.userAgent) { - lied = true; // documented in the worker source - } - return userAgent.trim().replace(/\s{2,}/, ' '); - }, 'userAgent failed'), - uaPostReduction: isUAPostReduction((navigator || {}).userAgent), - appVersion: attempt(() => { - const { appVersion } = navigator; - if (!credibleUserAgent) { - sendToTrash('appVersion', `${appVersion} does not match userAgent`); - } - if ('appVersion' in navigator && !appVersion) { - sendToTrash('appVersion', 'Living Standard property returned falsy value'); - } - if (/\s{2,}|^\s|\s$/g.test(appVersion)) { - sendToTrash('appVersion', `extra spaces detected`); - } - return appVersion.trim().replace(/\s{2,}/, ' '); - }, 'appVersion failed'), - deviceMemory: attempt(() => { - if (!('deviceMemory' in navigator)) { - return undefined; - } - // @ts-ignore - const { deviceMemory } = navigator; - const trusted = { - '0.25': true, - '0.5': true, - '1': true, - '2': true, - '4': true, - '8': true, - '16': true, - '32': true, - }; - if (!trusted[deviceMemory]) { - sendToTrash('deviceMemory', `${deviceMemory} is not a valid value [0.25, 0.5, 1, 2, 4, 8, 16, 32]`); - } - // @ts-expect-error memory is undefined if not supported - const memory = performance?.memory?.jsHeapSizeLimit || null; - const memoryInGigabytes = memory ? +(memory / 1073741824).toFixed(1) : 0; - if (memoryInGigabytes > deviceMemory) { - sendToTrash('deviceMemory', `available memory ${memoryInGigabytes}GB is greater than device memory ${deviceMemory}GB`); - } - if (deviceMemory !== workerScope.deviceMemory) { - lied = true; // documented in the worker source - } - return deviceMemory; - }, 'deviceMemory failed'), - doNotTrack: attempt(() => { - const { doNotTrack } = navigator; - const trusted = { - '1': !0, - 'true': !0, - 'yes': !0, - '0': !0, - 'false': !0, - 'no': !0, - 'unspecified': !0, - 'null': !0, - 'undefined': !0, - }; - if (!trusted[doNotTrack]) { - sendToTrash('doNotTrack - unusual result', doNotTrack); - } - return doNotTrack; - }, 'doNotTrack failed'), - globalPrivacyControl: attempt(() => { - if (!('globalPrivacyControl' in navigator)) { - return undefined; - } - // @ts-ignore - const { globalPrivacyControl } = navigator; - const trusted = { - '1': !0, - 'true': !0, - 'yes': !0, - '0': !0, - 'false': !0, - 'no': !0, - 'unspecified': !0, - 'null': !0, - 'undefined': !0, - }; - if (!trusted[globalPrivacyControl]) { - sendToTrash('globalPrivacyControl - unusual result', globalPrivacyControl); - } - return globalPrivacyControl; - }, 'globalPrivacyControl failed'), - hardwareConcurrency: attempt(() => { - if (!('hardwareConcurrency' in navigator)) { - return undefined; - } - const { hardwareConcurrency } = navigator; - if (hardwareConcurrency !== workerScope.hardwareConcurrency) { - lied = true; // documented in the worker source - } - return hardwareConcurrency; - }, 'hardwareConcurrency failed'), - language: attempt(() => { - const { language, languages } = navigator; - if (language && languages) { - // @ts-ignore - const lang = /^.{0,2}/g.exec(language)[0]; - // @ts-ignore - const langs = /^.{0,2}/g.exec(languages[0])[0]; - if (langs != lang) { - sendToTrash('language/languages', `${[language, languages].join(' ')} mismatch`); - } - return `${languages.join(', ')} (${language})`; - } - if (language != workerScope.language) { - lied = true; - documentLie(`Navigator.language`, `${language} does not match worker scope`); - } - if (languages !== workerScope.languages) { - lied = true; - documentLie(`Navigator.languages`, `${languages} does not match worker scope`); - } - return `${language} ${languages}`; - }, 'language(s) failed'), - maxTouchPoints: attempt(() => { - if (!('maxTouchPoints' in navigator)) { - return null; - } - return navigator.maxTouchPoints; - }, 'maxTouchPoints failed'), - vendor: attempt(() => navigator.vendor, 'vendor failed'), - mimeTypes: attempt(() => { - const { mimeTypes } = navigator; - return mimeTypes ? [...mimeTypes].map((m) => m.type) : []; - }, 'mimeTypes failed'), - // @ts-ignore - oscpu: attempt(() => navigator.oscpu, 'oscpu failed'), - plugins: attempt(() => { - // https://html.spec.whatwg.org/multipage/system-state.html#pdf-viewing-support - const { plugins } = navigator; - if (!(plugins instanceof PluginArray)) { - return; - } - const response = plugins ? [...plugins] - .map((p) => ({ - name: p.name, - description: p.description, - filename: p.filename, - // @ts-ignore - version: p.version, - })) : []; - const { lies } = getPluginLies(plugins, navigator.mimeTypes); - if (lies.length) { - lied = true; - lies.forEach((lie) => { - return documentLie(`Navigator.plugins`, lie); - }); - } - if (response.length) { - response.forEach((plugin) => { - const { name, description } = plugin; - const nameGibbers = gibberish(name); - const descriptionGibbers = gibberish(description); - if (nameGibbers.length) { - sendToTrash(`plugin name is gibberish`, name); - } - if (descriptionGibbers.length) { - sendToTrash(`plugin description is gibberish`, description); - } - return; - }); - } - return response; - }, 'plugins failed'), - properties: attempt(() => { - const keys = Object.keys(Object.getPrototypeOf(navigator)); - return keys; - }, 'navigator keys failed'), - }; - const getUserAgentData = () => attempt(() => { - // @ts-ignore - if (!navigator.userAgentData || - // @ts-ignore - !navigator.userAgentData.getHighEntropyValues) { - return; - } - // @ts-ignore - return navigator.userAgentData.getHighEntropyValues(['platform', 'platformVersion', 'architecture', 'bitness', 'model', 'uaFullVersion']).then((data) => { - // @ts-ignore - const { brands, mobile } = navigator.userAgentData || {}; - const compressedBrands = (brands, captureVersion = false) => brands - .filter((obj) => !/Not/.test(obj.brand)).map((obj) => `${obj.brand}${captureVersion ? ` ${obj.version}` : ''}`); - const removeChromium = (brands) => (brands.length > 1 ? brands.filter((brand) => !/Chromium/.test(brand)) : brands); - // compress brands - if (!data.brands) { - data.brands = brands; - } - data.brandsVersion = compressedBrands(data.brands, true); - data.brands = compressedBrands(data.brands); - data.brandsVersion = removeChromium(data.brandsVersion); - data.brands = removeChromium(data.brands); - if (!data.mobile) { - data.mobile = mobile; - } - const dataSorted = Object.keys(data).sort().reduce((acc, key) => { - acc[key] = data[key]; - return acc; - }, {}); - return dataSorted; - }); - }, 'userAgentData failed'); - const getBluetoothAvailability = () => attempt(() => { - if (!('bluetooth' in navigator) || - // @ts-ignore - !navigator.bluetooth || - // @ts-ignore - !navigator.bluetooth.getAvailability) { - return undefined; - } - // @ts-ignore - return navigator.bluetooth.getAvailability(); - }, 'bluetoothAvailability failed'); - const getPermissions = () => attempt(() => { - const getPermissionState = (name) => navigator.permissions.query({ name }) - .then((res) => ({ name, state: res.state })) - .catch((error) => ({ name, state: 'unknown' })); - // https://w3c.github.io/permissions/#permission-registry - const permissions = !('permissions' in navigator) ? undefined : Promise.all([ - getPermissionState('accelerometer'), - getPermissionState('ambient-light-sensor'), - getPermissionState('background-fetch'), - getPermissionState('background-sync'), - getPermissionState('bluetooth'), - getPermissionState('camera'), - getPermissionState('clipboard'), - getPermissionState('device-info'), - getPermissionState('display-capture'), - getPermissionState('gamepad'), - getPermissionState('geolocation'), - getPermissionState('gyroscope'), - getPermissionState('magnetometer'), - getPermissionState('microphone'), - getPermissionState('midi'), - getPermissionState('nfc'), - getPermissionState('notifications'), - getPermissionState('persistent-storage'), - getPermissionState('push'), - getPermissionState('screen-wake-lock'), - getPermissionState('speaker'), - getPermissionState('speaker-selection'), - ]).then((permissions) => permissions.reduce((acc, perm) => { - const { state, name } = perm || {}; - if (acc[state]) { - acc[state].push(name); - return acc; - } - acc[state] = [name]; - return acc; - }, {})).catch((error) => console.error(error)); - return permissions; - }, 'permissions failed'); - const getWebGpu = () => attempt(() => { - if (!('gpu' in navigator)) { - return; - } - // @ts-expect-error if unsupported - return navigator.gpu.requestAdapter().then((adapter) => { - if (!adapter) - return; - const { limits = {}, features = [] } = adapter || {}; - // @ts-expect-error if unsupported - const handleInfo = (info) => { - const { architecture, description, device, vendor } = info; - const adapterInfo = [vendor, architecture, description, device]; - const featureValues = [...features.values()]; - const limitsData = ((limits) => { - const data = {}; - // eslint-disable-next-line guard-for-in - for (const prop in limits) { - data[prop] = limits[prop]; - } - return data; - })(limits); - Analysis.webGpuAdapter = adapterInfo; - Analysis.webGpuFeatures = featureValues; - Analysis.webGpuLimits = hashMini(limitsData); - return { - adapterInfo, - limits: limitsData, - }; - }; - const { info } = adapter; - return info ? handleInfo(info) : adapter.requestAdapterInfo().then(handleInfo); - }); - }, 'webgpu failed'); - await queueEvent(timer); - return Promise.all([ - getUserAgentData(), - getBluetoothAvailability(), - getPermissions(), - getWebGpu(), - ]).then(([userAgentData, bluetoothAvailability, permissions, webgpu,]) => { - logTestResult({ time: timer.stop(), test: 'navigator', passed: true }); - return { - ...data, - userAgentData, - bluetoothAvailability, - permissions, - webgpu, - lied, - }; - }).catch((error) => { - console.error(error); - logTestResult({ time: timer.stop(), test: 'navigator', passed: true }); - return { - ...data, - lied, - }; - }); - } - catch (error) { - logTestResult({ test: 'navigator', passed: false }); - captureError(error, 'Navigator failed or blocked by client'); - return; - } - } - function navigatorHTML(fp) { - if (!fp.navigator) { - return ` -
- Navigator -
properties (0): ${HTMLNote.BLOCKED}
-
dnt: ${HTMLNote.BLOCKED}
-
gpc:${HTMLNote.BLOCKED}
-
lang: ${HTMLNote.BLOCKED}
-
mimeTypes (0): ${HTMLNote.BLOCKED}
-
permissions (0): ${HTMLNote.BLOCKED}
-
plugins (0): ${HTMLNote.BLOCKED}
-
vendor: ${HTMLNote.BLOCKED}
-
webgpu: ${HTMLNote.BLOCKED}
-
userAgentData:
-
${HTMLNote.BLOCKED}
-
-
-
device:
-
${HTMLNote.BLOCKED}
-
ua parsed: ${HTMLNote.BLOCKED}
-
userAgent:
-
${HTMLNote.BLOCKED}
-
appVersion:
-
${HTMLNote.BLOCKED}
-
`; - } - const { navigator: { $hash, appVersion, deviceMemory, doNotTrack, globalPrivacyControl, hardwareConcurrency, language, maxTouchPoints, mimeTypes, oscpu, permissions, platform, plugins, properties, system, device, userAgent, uaPostReduction, userAgentData, userAgentParsed, vendor, bluetoothAvailability, webgpu, lied, }, } = fp; - const id = 'creep-navigator'; - const blocked = { - ['null']: true, - ['undefined']: true, - ['']: true, - }; - const permissionsKeys = Object.keys(permissions || {}); - const permissionsGranted = (permissions && permissions.granted ? permissions.granted.length : 0); - return ` - ${performanceLogger.getLog().navigator} -
- Navigator${hashSlice($hash)} -
properties (${count(properties)}): ${modal(`${id}-properties`, properties.join(', '), hashMini(properties))}
-
dnt: ${'' + doNotTrack}
-
gpc: ${'' + globalPrivacyControl == 'undefined' ? HTMLNote.UNSUPPORTED : '' + globalPrivacyControl}
-
lang: ${!blocked[language] ? language : HTMLNote.BLOCKED}
-
mimeTypes (${count(mimeTypes)}): ${!blocked['' + mimeTypes] ? - modal(`${id}-mimeTypes`, mimeTypes.join('
'), hashMini(mimeTypes)) : - HTMLNote.BLOCKED}
-
permissions (${'' + permissionsGranted}): ${!permissions || !permissionsKeys ? HTMLNote.UNSUPPORTED : modal('creep-permissions', permissionsKeys.map((key) => `
${key}:
${permissions[key].join('
')}
`).join(''), hashMini(permissions))}
-
plugins (${count(plugins)}): ${!blocked['' + plugins] ? - modal(`${id}-plugins`, plugins.map((plugin) => plugin.name).join('
'), hashMini(plugins)) : - HTMLNote.BLOCKED}
-
vendor: ${!blocked[vendor] ? vendor : HTMLNote.BLOCKED}
-
webgpu: ${!webgpu ? HTMLNote.UNSUPPORTED : - modal(`${id}-webgpu`, ((webgpu) => { - const { adapterInfo, limits } = webgpu; - return ` -
- Adapter
${adapterInfo.filter((x) => x).join('
')} -
-
-
Limits
${Object.keys(limits).map((x) => `${x}: ${limits[x]}`).join('
')} -
- `; - })(webgpu), hashMini(webgpu))}
-
userAgentData:
-
-
- ${((userAgentData) => { - const { architecture, bitness, brandsVersion, uaFullVersion, mobile, model, platformVersion, platform, } = userAgentData || {}; - // @ts-ignore - const windowsRelease = computeWindowsRelease({ platform, platformVersion }); - return !userAgentData ? HTMLNote.UNSUPPORTED : ` - ${(brandsVersion || []).join(',')}${uaFullVersion ? ` (${uaFullVersion})` : ''} -
${windowsRelease || `${platform} ${platformVersion}`} ${architecture ? `${architecture}${bitness ? `_${bitness}` : ''}` : ''} - ${model ? `
${model}` : ''} - ${mobile ? '
mobile' : ''} - `; - })(userAgentData)} -
-
-
-
-
device:
-
- ${oscpu ? oscpu : ''} - ${`${oscpu ? '
' : ''}${system}${platform ? ` (${platform})` : ''}`} - ${device ? `
${device}` : HTMLNote.BLOCKED}${hardwareConcurrency && deviceMemory ? `
cores: ${hardwareConcurrency}, ram: ${deviceMemory}` : - hardwareConcurrency && !deviceMemory ? `
cores: ${hardwareConcurrency}` : - !hardwareConcurrency && deviceMemory ? `
ram: ${deviceMemory}` : ''}${typeof maxTouchPoints != 'undefined' ? `, touch: ${'' + maxTouchPoints}` : ''}${bluetoothAvailability ? `, bluetooth` : ''} -
-
ua parsed: ${userAgentParsed || HTMLNote.BLOCKED}
-
userAgent:${!uaPostReduction ? '' : `ua reduction`}
-
-
${userAgent || HTMLNote.BLOCKED}
-
-
appVersion:
-
-
${appVersion || HTMLNote.BLOCKED}
-
-
- `; - } - - async function getResistance() { - try { - const timer = createTimer(); - await queueEvent(timer); - const data = { - privacy: undefined, - security: undefined, - mode: undefined, - extension: undefined, - engine: (IS_BLINK ? 'Blink' : - IS_GECKO ? 'Gecko' : - ''), - }; - // Firefox/Tor Browser - const regex = (n) => new RegExp(`${n}+$`); - const delay = (ms, baseNumber, baseDate) => new Promise((resolve) => setTimeout(() => { - const date = baseDate ? baseDate : +new Date(); - // @ts-ignore - const value = regex(baseNumber).test(date) ? regex(baseNumber).exec(date)[0] : date; - return resolve(value); - }, ms)); - const getTimerPrecision = async () => { - const baseDate = +new Date(); - const baseNumber = +('' + baseDate).slice(-1); - const a = await delay(0, baseNumber, baseDate); - const b = await delay(1, baseNumber); - const c = await delay(2, baseNumber); - const d = await delay(3, baseNumber); - const e = await delay(4, baseNumber); - const f = await delay(5, baseNumber); - const g = await delay(6, baseNumber); - const h = await delay(7, baseNumber); - const i = await delay(8, baseNumber); - const j = await delay(9, baseNumber); - const lastCharA = ('' + a).slice(-1); - const lastCharB = ('' + b).slice(-1); - const lastCharC = ('' + c).slice(-1); - const lastCharD = ('' + d).slice(-1); - const lastCharE = ('' + e).slice(-1); - const lastCharF = ('' + f).slice(-1); - const lastCharG = ('' + g).slice(-1); - const lastCharH = ('' + h).slice(-1); - const lastCharI = ('' + i).slice(-1); - const lastCharJ = ('' + j).slice(-1); - const protection = (lastCharA == lastCharB && - lastCharA == lastCharC && - lastCharA == lastCharD && - lastCharA == lastCharE && - lastCharA == lastCharF && - lastCharA == lastCharG && - lastCharA == lastCharH && - lastCharA == lastCharI && - lastCharA == lastCharJ); - const baseLen = ('' + a).length; - const collection = [a, b, c, d, e, f, g, h, i, j]; - return { - protection, - delays: collection.map((n) => ('' + n).length > baseLen ? ('' + n).slice(-baseLen) : n), - precision: protection ? Math.min(...collection.map((val) => ('' + val).length)) : undefined, - precisionValue: protection ? lastCharA : undefined, - }; - }; - const [isBrave, timerPrecision,] = await Promise.all([ - braveBrowser(), - IS_BLINK ? undefined : getTimerPrecision(), - ]); - if (isBrave) { - const braveMode = getBraveMode(); - data.privacy = 'Brave'; - // @ts-ignore - data.security = { - 'FileSystemWritableFileStream': 'FileSystemWritableFileStream' in window, - 'Serial': 'Serial' in window, - 'ReportingObserver': 'ReportingObserver' in window, - }; - data.mode = (braveMode.allow ? 'allow' : - braveMode.standard ? 'standard' : - braveMode.strict ? 'strict' : - ''); - } - const { protection } = timerPrecision || {}; - if (IS_GECKO && protection) { - const features = { - 'OfflineAudioContext': 'OfflineAudioContext' in window, // dom.webaudio.enabled - 'WebGL2RenderingContext': 'WebGL2RenderingContext' in window, // webgl.enable-webgl2 - 'WebAssembly': 'WebAssembly' in window, // javascript.options.wasm - 'maxTouchPoints': 'maxTouchPoints' in navigator, - 'RTCRtpTransceiver': 'RTCRtpTransceiver' in window, - 'MediaDevices': 'MediaDevices' in window, - 'Credential': 'Credential' in window, - }; - const featureKeys = Object.keys(features); - const targetSet = new Set([ - 'RTCRtpTransceiver', - 'MediaDevices', - 'Credential', - ]); - const torBrowser = featureKeys.filter((key) => targetSet.has(key) && !features[key]).length == targetSet.size; - const safer = !features.WebAssembly; - data.privacy = torBrowser ? 'Tor Browser' : 'Firefox'; - // @ts-ignore - data.security = { - 'reduceTimerPrecision': true, - ...features, - }; - data.mode = (!torBrowser ? 'resistFingerprinting' : - safer ? 'safer' : - 'standard'); - } - // extension - // - this technique gets a small sample of known lie patterns - // - patterns vary based on extensions settings, version, browser - const prototypeLiesLen = Object.keys(prototypeLies).length; - // patterns based on settings - const disabled = 'c767712b'; - const pattern = { - noscript: { - contentDocumentHash: ['0b637a33', '37e2f32e', '318390d1'], - contentWindowHash: ['0b637a33', '37e2f32e', '318390d1'], - getContextHash: ['0b637a33', '081d6d1b', disabled], - }, - trace: { - contentDocumentHash: ['ca9d9c2f'], - contentWindowHash: ['ca9d9c2f'], - createElementHash: ['77dea834'], - getElementByIdHash: ['77dea834'], - getImageDataHash: ['77dea834'], - toBlobHash: ['77dea834', disabled], - toDataURLHash: ['77dea834', disabled], - }, - cydec: { - // [FF, FF Anti OFF, Chrome, Chrome Anti Off, no iframe Chrome, no iframe Chrome Anti Off] - contentDocumentHash: ['945b0c78', '15771efa', '403a1a21', '55e9b959'], - contentWindowHash: ['945b0c78', '15771efa', '403a1a21', '55e9b959'], - createElementHash: ['3dd86d6f', 'cc7cb598', '4237b44c', '1466aaf0', '0cb0c682', '73c662d9', '72b1ee2b', 'ae3d02c9'], - getElementByIdHash: ['3dd86d6f', 'cc7cb598', '4237b44c', '1466aaf0', '0cb0c682', '73c662d9', '72b1ee2b', 'ae3d02c9'], - getImageDataHash: ['044f14c2', 'db60d7f9', '15771efa', 'db60d7f9', '55e9b959'], - toBlobHash: ['044f14c2', '15771efa', 'afec348d', '55e9b959', '0dbbf456'], - toDataURLHash: ['ecb498d9', '15771efa', '6b838fb6', 'd19104ec', '6985d315', '55e9b959', 'fe88259f'], - }, - canvasblocker: { - contentDocumentHash: ['98ec858e', 'dbbaf31f'], - contentWindowHash: ['98ec858e', 'dbbaf31f'], - appendHash: ['98ec858e', 'dbbaf31f'], - getImageDataHash: ['98ec858e', 'a2971888', 'dbbaf31f', disabled], - toBlobHash: ['9f1c3dfe', 'a2971888', 'dbbaf31f', disabled], - toDataURLHash: ['98ec858e', 'a2971888', 'dbbaf31f', disabled], - }, - chameleon: { - appendHash: ['77dea834'], - insertAdjacentElementHash: ['77dea834'], - insertAdjacentHTMLHash: ['77dea834'], - insertAdjacentTextHash: ['77dea834'], - prependHash: ['77dea834'], - replaceWithHash: ['77dea834'], - appendChildHash: ['77dea834'], - insertBeforeHash: ['77dea834'], - replaceChildHash: ['77dea834'], - }, - duckduckgo: { - toDataURLHash: ['fd00bf5d', '8ee7df22', disabled], - toBlobHash: ['fd00bf5d', '8ee7df22', disabled], - getImageDataHash: ['fd00bf5d', '8ee7df22', disabled], - getByteFrequencyDataHash: ['fd00bf5d', '8ee7df22', disabled], - getByteTimeDomainDataHash: ['fd00bf5d', '8ee7df22', disabled], - getFloatFrequencyDataHash: ['fd00bf5d', '8ee7df22', disabled], - getFloatTimeDomainDataHash: ['fd00bf5d', '8ee7df22', disabled], - copyFromChannelHash: ['fd00bf5d', '8ee7df22', disabled], - getChannelDataHash: ['fd00bf5d', '8ee7df22', disabled], - hardwareConcurrencyHash: ['dfd41ab4'], - availHeightHash: ['dfd41ab4'], - availLeftHash: ['dfd41ab4'], - availTopHash: ['dfd41ab4'], - availWidthHash: ['dfd41ab4'], - colorDepthHash: ['dfd41ab4'], - pixelDepthHash: ['dfd41ab4'], - }, - // mode: Learn to block new trackers from your browsing - privacybadger: { - getImageDataHash: ['0cb0c682'], - toDataURLHash: ['0cb0c682'], - }, - privacypossum: { - hardwareConcurrencyHash: ['452924d5'], - availWidthHash: ['452924d5'], - colorDepthHash: ['452924d5'], - }, - jshelter: { - contentDocumentHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'], - contentWindowHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'], - appendHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'], - insertAdjacentElementHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'], - insertAdjacentHTMLHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'], - prependHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'], - replaceWithHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'], - appendChildHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'], - insertBeforeHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'], - replaceChildHash: ['0007ab4e', '0b637a33', '866fa7e7', '318390d1'], - hardwareConcurrencyHash: ['dfd41ab4'], - }, - puppeteerExtra: { - contentDocumentHash: ['55e9b959'], - contentWindowHash: [ - '55e9b959', - '50a281b5', // @2.10.0 - ], - createElementHash: ['55e9b959'], - getElementByIdHash: ['55e9b959'], - appendHash: ['55e9b959'], - insertAdjacentElementHash: ['55e9b959'], - insertAdjacentHTMLHash: ['55e9b959'], - insertAdjacentTextHash: ['55e9b959'], - prependHash: ['55e9b959'], - replaceWithHash: ['55e9b959'], - appendChildHash: ['55e9b959'], - insertBeforeHash: ['55e9b959'], - replaceChildHash: ['55e9b959'], - getContextHash: ['55e9b959', disabled], - toDataURLHash: ['55e9b959', disabled], - toBlobHash: ['55e9b959', disabled], - getImageDataHash: ['55e9b959'], - hardwareConcurrencyHash: ['efbd4cf9', 'a63491fb', 'b011fd1c', '194ecf17', '55e9b959'], - }, - fakeBrowser: { - appendChildHash: ['8dfec2ec', 'f43e6134'], - getContextHash: ['83b825ab', 'a63491fb'], - toDataURLHash: ['83b825ab', 'a63491fb'], - toBlobHash: ['83b825ab', 'a63491fb'], - getImageDataHash: ['83b825ab', 'a63491fb'], - hardwareConcurrencyHash: ['83b825ab', 'a63491fb'], - availHeightHash: ['83b825ab', 'a63491fb'], - availLeftHash: ['83b825ab', 'a63491fb'], - availTopHash: ['83b825ab', 'a63491fb'], - availWidthHash: ['83b825ab', 'a63491fb'], - colorDepthHash: ['83b825ab', 'a63491fb'], - pixelDepthHash: ['83b825ab', 'a63491fb'], - }, - }; - /* - Random User-Agent - User Agent Switcher and Manager - ScriptSafe - Windscribe - */ - await queueEvent(timer); - const hash = { - // iframes - contentDocumentHash: hashMini(prototypeLies['HTMLIFrameElement.contentDocument']), - contentWindowHash: hashMini(prototypeLies['HTMLIFrameElement.contentWindow']), - createElementHash: hashMini(prototypeLies['Document.createElement']), - getElementByIdHash: hashMini(prototypeLies['Document.getElementById']), - appendHash: hashMini(prototypeLies['Element.append']), - insertAdjacentElementHash: hashMini(prototypeLies['Element.insertAdjacentElement']), - insertAdjacentHTMLHash: hashMini(prototypeLies['Element.insertAdjacentHTML']), - insertAdjacentTextHash: hashMini(prototypeLies['Element.insertAdjacentText']), - prependHash: hashMini(prototypeLies['Element.prepend']), - replaceWithHash: hashMini(prototypeLies['Element.replaceWith']), - appendChildHash: hashMini(prototypeLies['Node.appendChild']), - insertBeforeHash: hashMini(prototypeLies['Node.insertBefore']), - replaceChildHash: hashMini(prototypeLies['Node.replaceChild']), - // canvas - getContextHash: hashMini(prototypeLies['HTMLCanvasElement.getContext']), - toDataURLHash: hashMini(prototypeLies['HTMLCanvasElement.toDataURL']), - toBlobHash: hashMini(prototypeLies['HTMLCanvasElement.toBlob']), - getImageDataHash: hashMini(prototypeLies['CanvasRenderingContext2D.getImageData']), - // Audio - getByteFrequencyDataHash: hashMini(prototypeLies['AnalyserNode.getByteFrequencyData']), - getByteTimeDomainDataHash: hashMini(prototypeLies['AnalyserNode.getByteTimeDomainData']), - getFloatFrequencyDataHash: hashMini(prototypeLies['AnalyserNode.getFloatFrequencyData']), - getFloatTimeDomainDataHash: hashMini(prototypeLies['AnalyserNode.getFloatTimeDomainData']), - copyFromChannelHash: hashMini(prototypeLies['AudioBuffer.copyFromChannel']), - getChannelDataHash: hashMini(prototypeLies['AudioBuffer.getChannelData']), - // Hardware - hardwareConcurrencyHash: hashMini(prototypeLies['Navigator.hardwareConcurrency']), - // Screen - availHeightHash: hashMini(prototypeLies['Screen.availHeight']), - availLeftHash: hashMini(prototypeLies['Screen.availLeft']), - availTopHash: hashMini(prototypeLies['Screen.availTop']), - availWidthHash: hashMini(prototypeLies['Screen.availWidth']), - colorDepthHash: hashMini(prototypeLies['Screen.colorDepth']), - pixelDepthHash: hashMini(prototypeLies['Screen.pixelDepth']), - }; - data.extensionHashPattern = Object.keys(hash).reduce((acc, key) => { - const val = hash[key]; - if (val == disabled) { - return acc; - } - acc[key.replace('Hash', '')] = val; - return acc; - }, {}); - const getExtension = ({ pattern, hash, prototypeLiesLen }) => { - const { noscript, trace, cydec, canvasblocker, chameleon, duckduckgo, privacybadger, privacypossum, jshelter, puppeteerExtra, fakeBrowser, } = pattern; - const disabled = 'c767712b'; - if (prototypeLiesLen) { - if (prototypeLiesLen >= 7 && - trace.contentDocumentHash.includes(hash.contentDocumentHash) && - trace.contentWindowHash.includes(hash.contentWindowHash) && - trace.createElementHash.includes(hash.createElementHash) && - trace.getElementByIdHash.includes(hash.getElementByIdHash) && - trace.toDataURLHash.includes(hash.toDataURLHash) && - trace.toBlobHash.includes(hash.toBlobHash) && - trace.getImageDataHash.includes(hash.getImageDataHash)) { - return 'Trace'; - } - if (prototypeLiesLen >= 7 && - cydec.contentDocumentHash.includes(hash.contentDocumentHash) && - cydec.contentWindowHash.includes(hash.contentWindowHash) && - cydec.createElementHash.includes(hash.createElementHash) && - cydec.getElementByIdHash.includes(hash.getElementByIdHash) && - cydec.toDataURLHash.includes(hash.toDataURLHash) && - cydec.toBlobHash.includes(hash.toBlobHash) && - cydec.getImageDataHash.includes(hash.getImageDataHash)) { - return 'CyDec'; - } - if (prototypeLiesLen >= 6 && - canvasblocker.contentDocumentHash.includes(hash.contentDocumentHash) && - canvasblocker.contentWindowHash.includes(hash.contentWindowHash) && - canvasblocker.appendHash.includes(hash.appendHash) && - canvasblocker.toDataURLHash.includes(hash.toDataURLHash) && - canvasblocker.toBlobHash.includes(hash.toBlobHash) && - canvasblocker.getImageDataHash.includes(hash.getImageDataHash)) { - return 'CanvasBlocker'; - } - if (prototypeLiesLen >= 9 && - chameleon.appendHash.includes(hash.appendHash) && - chameleon.insertAdjacentElementHash.includes(hash.insertAdjacentElementHash) && - chameleon.insertAdjacentHTMLHash.includes(hash.insertAdjacentHTMLHash) && - chameleon.insertAdjacentTextHash.includes(hash.insertAdjacentTextHash) && - chameleon.prependHash.includes(hash.prependHash) && - chameleon.replaceWithHash.includes(hash.replaceWithHash) && - chameleon.appendChildHash.includes(hash.appendChildHash) && - chameleon.insertBeforeHash.includes(hash.insertBeforeHash) && - chameleon.replaceChildHash.includes(hash.replaceChildHash)) { - return 'Chameleon'; - } - if (prototypeLiesLen >= 7 && - duckduckgo.toDataURLHash.includes(hash.toDataURLHash) && - duckduckgo.toBlobHash.includes(hash.toBlobHash) && - duckduckgo.getImageDataHash.includes(hash.getImageDataHash) && - duckduckgo.getByteFrequencyDataHash.includes(hash.getByteFrequencyDataHash) && - duckduckgo.getByteTimeDomainDataHash.includes(hash.getByteTimeDomainDataHash) && - duckduckgo.getFloatFrequencyDataHash.includes(hash.getFloatFrequencyDataHash) && - duckduckgo.getFloatTimeDomainDataHash.includes(hash.getFloatTimeDomainDataHash) && - duckduckgo.copyFromChannelHash.includes(hash.copyFromChannelHash) && - duckduckgo.getChannelDataHash.includes(hash.getChannelDataHash) && - duckduckgo.hardwareConcurrencyHash.includes(hash.hardwareConcurrencyHash) && - duckduckgo.availHeightHash.includes(hash.availHeightHash) && - duckduckgo.availLeftHash.includes(hash.availLeftHash) && - duckduckgo.availTopHash.includes(hash.availTopHash) && - duckduckgo.availWidthHash.includes(hash.availWidthHash) && - duckduckgo.colorDepthHash.includes(hash.colorDepthHash) && - duckduckgo.pixelDepthHash.includes(hash.pixelDepthHash)) { - return 'DuckDuckGo'; - } - if (prototypeLiesLen >= 2 && - privacybadger.getImageDataHash.includes(hash.getImageDataHash) && - privacybadger.toDataURLHash.includes(hash.toDataURLHash)) { - return 'Privacy Badger'; - } - if (prototypeLiesLen >= 3 && - privacypossum.hardwareConcurrencyHash.includes(hash.hardwareConcurrencyHash) && - privacypossum.availWidthHash.includes(hash.availWidthHash) && - privacypossum.colorDepthHash.includes(hash.colorDepthHash)) { - return 'Privacy Possum'; - } - if (prototypeLiesLen >= 2 && - noscript.contentDocumentHash.includes(hash.contentDocumentHash) && - noscript.contentWindowHash.includes(hash.contentDocumentHash) && - noscript.getContextHash.includes(hash.getContextHash) && - // distinguish NoScript from JShelter - hash.hardwareConcurrencyHash == disabled) { - return 'NoScript'; - } - if (prototypeLiesLen >= 14 && - jshelter.contentDocumentHash.includes(hash.contentDocumentHash) && - jshelter.contentWindowHash.includes(hash.contentDocumentHash) && - jshelter.appendHash.includes(hash.appendHash) && - jshelter.insertAdjacentElementHash.includes(hash.insertAdjacentElementHash) && - jshelter.insertAdjacentHTMLHash.includes(hash.insertAdjacentHTMLHash) && - jshelter.prependHash.includes(hash.prependHash) && - jshelter.replaceWithHash.includes(hash.replaceWithHash) && - jshelter.appendChildHash.includes(hash.appendChildHash) && - jshelter.insertBeforeHash.includes(hash.insertBeforeHash) && - jshelter.replaceChildHash.includes(hash.replaceChildHash) && - jshelter.hardwareConcurrencyHash.includes(hash.hardwareConcurrencyHash)) { - return 'JShelter'; - } - if (prototypeLiesLen >= 13 && - puppeteerExtra.contentDocumentHash.includes(hash.contentDocumentHash) && - puppeteerExtra.contentWindowHash.includes(hash.contentWindowHash) && - puppeteerExtra.createElementHash.includes(hash.createElementHash) && - puppeteerExtra.getElementByIdHash.includes(hash.getElementByIdHash) && - puppeteerExtra.appendHash.includes(hash.appendHash) && - puppeteerExtra.insertAdjacentElementHash.includes(hash.insertAdjacentElementHash) && - puppeteerExtra.insertAdjacentHTMLHash.includes(hash.insertAdjacentHTMLHash) && - puppeteerExtra.insertAdjacentTextHash.includes(hash.insertAdjacentTextHash) && - puppeteerExtra.prependHash.includes(hash.prependHash) && - puppeteerExtra.replaceWithHash.includes(hash.replaceWithHash) && - puppeteerExtra.appendChildHash.includes(hash.appendChildHash) && - puppeteerExtra.insertBeforeHash.includes(hash.insertBeforeHash) && - puppeteerExtra.contentDocumentHash.includes(hash.contentDocumentHash) && - puppeteerExtra.replaceChildHash.includes(hash.replaceChildHash) && - puppeteerExtra.getContextHash.includes(hash.getContextHash) && - puppeteerExtra.toDataURLHash.includes(hash.toDataURLHash) && - puppeteerExtra.toBlobHash.includes(hash.toBlobHash) && - puppeteerExtra.getImageDataHash.includes(hash.getImageDataHash) && - puppeteerExtra.hardwareConcurrencyHash.includes(hash.hardwareConcurrencyHash)) { - return 'puppeteer-extra'; - } - if (prototypeLiesLen >= 12 && - fakeBrowser.appendChildHash.includes(hash.appendChildHash) && - fakeBrowser.getContextHash.includes(hash.getContextHash) && - fakeBrowser.toDataURLHash.includes(hash.toDataURLHash) && - fakeBrowser.toBlobHash.includes(hash.toBlobHash) && - fakeBrowser.getImageDataHash.includes(hash.getImageDataHash) && - fakeBrowser.hardwareConcurrencyHash.includes(hash.hardwareConcurrencyHash) && - fakeBrowser.availHeightHash.includes(hash.availHeightHash) && - fakeBrowser.availLeftHash.includes(hash.availLeftHash) && - fakeBrowser.availTopHash.includes(hash.availTopHash) && - fakeBrowser.availWidthHash.includes(hash.availWidthHash) && - fakeBrowser.colorDepthHash.includes(hash.colorDepthHash) && - fakeBrowser.pixelDepthHash.includes(hash.pixelDepthHash)) { - return 'FakeBrowser'; - } - return; - } - return; - }; - // @ts-ignore - data.extension = getExtension({ pattern, hash, prototypeLiesLen }); - logTestResult({ time: timer.stop(), test: 'resistance', passed: true }); - return data; - } - catch (error) { - logTestResult({ test: 'resistance', passed: false }); - captureError(error); - return; - } - } - function resistanceHTML(fp) { - if (!fp.resistance) { - return ` -
- Resistance -
privacy: ${HTMLNote.BLOCKED}
-
security: ${HTMLNote.BLOCKED}
-
mode: ${HTMLNote.BLOCKED}
-
extension: ${HTMLNote.BLOCKED}
-
`; - } - const { resistance: data, } = fp; - const { $hash, privacy, security, mode, extension, extensionHashPattern, engine, } = data || {}; - const securitySettings = !security || Object.keys(security).reduce((acc, curr) => { - if (security[curr]) { - acc[curr] = 'enabled'; - return acc; - } - acc[curr] = 'disabled'; - return acc; - }, {}); - const browserIcon = (/brave/i.test(privacy) ? '' : - /tor/i.test(privacy) ? '' : - /firefox/i.test(privacy) ? '' : - ''); - const extensionIcon = (/blink/i.test(engine) ? '' : - /gecko/i.test(engine) ? '' : - ''); - return ` -
- ${performanceLogger.getLog().resistance} - Resistance${hashSlice($hash)} -
privacy: ${privacy ? `${browserIcon}${privacy}` : HTMLNote.UNKNOWN}
-
security: ${!security ? HTMLNote.UNKNOWN : - modal('creep-resistance', 'Security

' + - Object.keys(securitySettings).map((key) => `${key}: ${'' + securitySettings[key]}`).join('
'), hashMini(security))}
-
mode: ${mode || HTMLNote.UNKNOWN}
-
extension: ${!Object.keys(extensionHashPattern || {}).length ? HTMLNote.UNKNOWN : - modal('creep-extension', 'Pattern

' + - Object.keys(extensionHashPattern).map((key) => `${key}: ${'' + extensionHashPattern[key]}`).join('
'), (extension ? `${extensionIcon}${extension}` : hashMini(extensionHashPattern)))}
-
- `; - } - - function hasTouch() { - try { - return 'ontouchstart' in window && !!document.createEvent('TouchEvent'); - } - catch (err) { - return false; - } - } - async function getScreen(log = true) { - try { - const timer = createTimer(); - timer.start(); - let lied = (lieProps['Screen.width'] || - lieProps['Screen.height'] || - lieProps['Screen.availWidth'] || - lieProps['Screen.availHeight'] || - lieProps['Screen.colorDepth'] || - lieProps['Screen.pixelDepth']) || false; - const s = (window.screen || {}); - const { width, height, availWidth, availHeight, colorDepth, pixelDepth, } = s; - const dpr = window.devicePixelRatio || 0; - const firefoxWithHighDPR = IS_GECKO && (dpr != 1); - if (!firefoxWithHighDPR) { - // firefox with high dpr requires floating point precision dimensions - const matchMediaLie = !matchMedia(`(device-width: ${width}px) and (device-height: ${height}px)`).matches; - if (matchMediaLie) { - lied = true; - documentLie('Screen', 'failed matchMedia'); - } - } - const hasLiedDPR = !matchMedia(`(resolution: ${dpr}dppx)`).matches; - if (!IS_WEBKIT && hasLiedDPR) { - lied = true; - documentLie('Window.devicePixelRatio', 'lied dpr'); - } - const noTaskbar = !(width - availWidth || height - availHeight); - if (width > 800 && noTaskbar) { - LowerEntropy.SCREEN = true; - } - const data = { - width, - height, - availWidth, - availHeight, - colorDepth, - pixelDepth, - touch: hasTouch(), - lied, - }; - log && logTestResult({ time: timer.stop(), test: 'screen', passed: true }); - return data; - } - catch (error) { - log && logTestResult({ test: 'screen', passed: false }); - captureError(error); - return; - } - } - function screenHTML(fp) { - if (!fp.screen) { - return ` -
- Screen -
...screen: ${HTMLNote.BLOCKED}
-
....avail: ${HTMLNote.BLOCKED}
-
touch: ${HTMLNote.BLOCKED}
-
depth: ${HTMLNote.BLOCKED}
-
viewport: ${HTMLNote.BLOCKED}
-
-
`; - } - const { screen: data, } = fp; - const { $hash } = data || {}; - const perf = performanceLogger.getLog().screen; - const paintScreen = (event) => { - const el = document.getElementById('creep-resize'); - if (!el) { - return; - } - removeEventListener('resize', paintScreen); - return getScreen(false).then((data) => { - requestAnimationFrame(() => patch(el, html `${resizeHTML(({ data, $hash, perf, paintScreen }))}`)); - }); - }; - const resizeHTML = ({ data, $hash, perf, paintScreen }) => { - const { width, height, availWidth, availHeight, colorDepth, pixelDepth, touch, lied, } = data; - addEventListener('resize', paintScreen); - const s = (window.screen || {}); - const { orientation } = s; - const { type: orientationType } = orientation || {}; - const dpr = window.devicePixelRatio || undefined; - const { width: vVWidth, height: vVHeight } = (window.visualViewport || {}); - const mediaOrientation = !window.matchMedia ? undefined : (matchMedia('(orientation: landscape)').matches ? 'landscape' : - matchMedia('(orientation: portrait)').matches ? 'portrait' : undefined); - const displayMode = !window.matchMedia ? undefined : (matchMedia('(display-mode: fullscreen)').matches ? 'fullscreen' : - matchMedia('(display-mode: standalone)').matches ? 'standalone' : - matchMedia('(display-mode: minimal-ui)').matches ? 'minimal-ui' : - matchMedia('(display-mode: browser)').matches ? 'browser' : undefined); - const getDeviceDimensions = (width, height, diameter = 180) => { - const aspectRatio = width / height; - const isPortrait = height > width; - const deviceWidth = isPortrait ? diameter * aspectRatio : diameter; - const deviceHeight = isPortrait ? diameter : diameter / aspectRatio; - return { deviceWidth, deviceHeight }; - }; - // const { deviceWidth, deviceHeight } = getDeviceDimensions(width, height) - const { deviceWidth: deviceInnerWidth, deviceHeight: deviceInnerHeight } = getDeviceDimensions(innerWidth, innerHeight); - const toFix = (n, nFix) => { - const d = +(1 + [...Array(nFix)].map((x) => 0).join('')); - return Math.round(n * d) / d; - }; - const viewportTitle = `Window.outerWidth\nWindow.outerHeight\nWindow.innerWidth\nWindow.innerHeight\nVisualViewport.width\nVisualViewport.height\nWindow.matchMedia()\nScreenOrientation.type\nWindow.devicePixelRatio`; - return ` -
- ${perf} - Screen${hashSlice($hash)} -
...screen: ${width} x ${height}
-
....avail: ${availWidth} x ${availHeight}
-
touch: ${touch}
-
depth: ${colorDepth}|${pixelDepth}
-
viewport:
-
- - ${outerWidth} - ${innerWidth} - ${toFix(vVWidth, 6)} - ${outerHeight} - ${innerHeight} - ${toFix(vVHeight, 6)} - ${displayMode} - ${mediaOrientation} - ${orientationType} - ${dpr} -
-
-
-
-
- `; - }; - return ` - ${resizeHTML({ data, $hash, perf, paintScreen })} - `; - } - - async function getVoices() { - // Don't run voice immediately. This is unstable - // wait a bit for services to load - await new Promise((resolve) => setTimeout(() => resolve(undefined), 50)); - return new Promise(async (resolve) => { - try { - const timer = createTimer(); - await queueEvent(timer); - // use window since iframe is unstable in FF - const supported = 'speechSynthesis' in window; - supported && speechSynthesis.getVoices(); // warm up - if (!supported) { - logTestResult({ test: 'speech', passed: false }); - return resolve(null); - } - const voicesLie = !!lieProps['SpeechSynthesis.getVoices']; - const giveUpOnVoices = setTimeout(() => { - logTestResult({ test: 'speech', passed: false }); - return resolve(null); - }, 300); - const getVoices = () => { - const data = speechSynthesis.getVoices(); - const localServiceDidLoad = (data || []).find((x) => x.localService); - if (!data || !data.length || (IS_BLINK && !localServiceDidLoad)) { - return; - } - clearTimeout(giveUpOnVoices); - // filter first occurrence of unique voiceURI data - const getUniques = (data, voiceURISet) => data - .filter((x) => { - const { voiceURI } = x; - if (!voiceURISet.has(voiceURI)) { - voiceURISet.add(voiceURI); - return true; - } - return false; - }); - const dataUnique = getUniques(data, new Set()); - // https://wicg.github.io/speech-api/#speechsynthesisvoice-attributes - const local = dataUnique.filter((x) => x.localService).map((x) => x.name); - const remote = dataUnique.filter((x) => !x.localService).map((x) => x.name); - const languages = [...new Set(dataUnique.map((x) => x.lang))]; - const defaultLocalVoices = dataUnique.filter((x) => x.default && x.localService); - let defaultVoiceName = ''; - let defaultVoiceLang = ''; - if (defaultLocalVoices.length === 1) { - const { name, lang } = defaultLocalVoices[0]; - defaultVoiceName = name; - defaultVoiceLang = (lang || '').replace(/_/, '-'); - } - // eslint-disable-next-line new-cap - const { locale: localeLang } = Intl.DateTimeFormat().resolvedOptions(); - if (defaultVoiceLang && - defaultVoiceLang.split('-')[0] !== localeLang.split('-')[0]) { - // this is not trash - Analysis.voiceLangMismatch = true; - LowerEntropy.TIME_ZONE = true; - } - logTestResult({ time: timer.stop(), test: 'speech', passed: true }); - return resolve({ - local, - remote, - languages, - defaultVoiceName, - defaultVoiceLang, - lied: voicesLie, - }); - }; - getVoices(); - if (speechSynthesis.addEventListener) { - return speechSynthesis.addEventListener('voiceschanged', getVoices); - } - speechSynthesis.onvoiceschanged = getVoices; - } - catch (error) { - logTestResult({ test: 'speech', passed: false }); - captureError(error); - return resolve(null); - } - }); - } - function voicesHTML(fp) { - if (!fp.voices) { - return ` -
- Speech -
local (0): ${HTMLNote.BLOCKED}
-
remote (0): ${HTMLNote.BLOCKED}
-
lang (0): ${HTMLNote.BLOCKED}
-
default:
-
${HTMLNote.BLOCKED}
-
`; - } - const { voices: { $hash, local, remote, languages, defaultVoiceName, defaultVoiceLang, lied, }, } = fp; - const icon = { - 'Linux': '', - 'Apple': '', - 'Windows': '', - 'Android': '', - 'CrOS': '', - }; - const system = { - 'Chrome OS': icon.CrOS, - 'Maged': icon.Apple, - 'Microsoft': icon.Windows, - 'English United States': icon.Android, - 'English (United States)': icon.Android, - }; - const systemVoice = Object.keys(system).find((key) => local.find((voice) => voice.includes(key))) || ''; - return ` -
- ${performanceLogger.getLog().speech} - Speech${hashSlice($hash)} -
local (${count(local)}): ${!local || !local.length ? HTMLNote.UNSUPPORTED : - modal('creep-voices-local', local.join('
'), `${system[systemVoice] || ''}${hashMini(local)}`)}
-
remote (${count(remote)}): ${!remote || !remote.length ? HTMLNote.UNSUPPORTED : - modal('creep-voices-remote', remote.join('
'), hashMini(remote))}
-
lang (${count(languages)}): ${!languages || !languages.length ? HTMLNote.BLOCKED : - languages.length == 1 ? languages[0] : modal('creep-voices-languages', languages.join('
'), hashMini(languages))}
-
default:
-
- ${!defaultVoiceName ? HTMLNote.UNSUPPORTED : - `${defaultVoiceName}${defaultVoiceLang ? ` [${defaultVoiceLang}]` : ''}`} -
-
- `; - } - - const GIGABYTE = 1073741824; // bytes - function getMaxCallStackSize() { - const fn = () => { - try { - return 1 + fn(); - } - catch (err) { - return 1; - } - }; - [...Array(10)].forEach(() => fn()); // stabilize - return fn(); - } - // based on and inspired by - // https://github.com/Joe12387/OP-Fingerprinting-Script/blob/main/opfs.js#L443 - function getTimingResolution() { - const maxRuns = 5000; - let valA = 1; - let valB = 1; - let res; - for (let i = 0; i < maxRuns; i++) { - const a = performance.now(); - const b = performance.now(); - if (a < b) { - res = b - a; - if (res > valA && res < valB) { - valB = res; - } - else if (res < valA) { - valB = valA; - valA = res; - } - } - } - return [valA, valB]; - } - function getClientLitter() { - try { - const iframe = document.createElement('iframe'); - document.body.appendChild(iframe); - const iframeWindow = iframe.contentWindow; - const windowKeys = Object.getOwnPropertyNames(window); - const iframeKeys = Object.getOwnPropertyNames(iframeWindow); - document.body.removeChild(iframe); - const clientKeys = windowKeys.filter((x) => !iframeKeys.includes(x)); - return clientKeys; - } - catch (err) { - return []; - } - } - function getClientCode() { - const names = Object.getOwnPropertyNames(window).slice(-50); - const [p1, p2] = (1).constructor.toString().split((1).constructor.name); - const isEngine = (fn) => { - return (typeof fn === 'function' && - ('' + fn === p1 + fn.name + p2 || '' + fn === p1 + (fn.name || '').replace('get ', '') + p2)); - }; - const isClient = (key) => { - if (/_$/.test(key)) - return true; - const d = Object.getOwnPropertyDescriptor(window, key); - if (!d) - return true; - return key === 'chrome' ? names.includes(key) : !isEngine(d.get || d.value); - }; - return Object.keys(window) - .slice(-50) - .filter((x) => isClient(x)); - } - async function getBattery() { - if (!('getBattery' in navigator)) - return null; - // @ts-expect-error if not supported - return navigator.getBattery(); - } - async function getStorage() { - if (!navigator?.storage?.estimate) - return null; - return Promise.all([ - navigator.storage.estimate().then(({ quota }) => quota), - new Promise((resolve) => { - // @ts-expect-error if not supported - navigator.webkitTemporaryStorage.queryUsageAndQuota((_, quota) => { - resolve(quota); - }); - }).catch(() => null), - ]).then(([quota1, quota2]) => (quota2 || quota1)); - } - async function getScriptSize() { - let url = null; - try { - // @ts-expect-error if unsupported - url = document?.currentScript?.src || (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('creep.js', document.baseURI).href); - } - catch (err) { } - if (!url) - return null; - return fetch(url) - .then((res) => res.blob()) - .then((blob) => blob.size) - .catch(() => null); - } - async function getStatus() { - const [batteryInfo, quotaA, quotaB, scriptSize, stackSize, timingRes, clientLitter,] = await Promise.all([ - getBattery(), - getStorage(), - getStorage(), - getScriptSize(), - getMaxCallStackSize(), - getTimingResolution(), - [...new Set([...getClientLitter(), ...getClientCode()])].sort().slice(0, 50), - ]); - // BatteryManager - const { charging, chargingTime, dischargingTime, level, } = batteryInfo || {}; - // MemoryInfo - // @ts-expect-error if not supported - const memory = performance?.memory?.jsHeapSizeLimit || null; - const memoryInGigabytes = memory ? +(memory / GIGABYTE).toFixed(2) : null; - // StorageManager - const quotaInGigabytes = quotaA ? +(+(quotaA) / GIGABYTE).toFixed(2) : null; - // Network Info - const { downlink, effectiveType, rtt, saveData, downlinkMax, type, - // @ts-expect-error if not supported - } = navigator?.connection || {}; - const scripts = [ - ...document.querySelectorAll('script'), - ].map((x) => x.src.replace(/^https?:\/\//, '')).slice(0, 10); - return { - charging, - chargingTime, - dischargingTime, - level, - memory, - memoryInGigabytes, - quota: quotaA, - quotaIsInsecure: quotaA !== quotaB, - quotaInGigabytes, - downlink, - effectiveType, - rtt, - saveData, - downlinkMax, - type, - stackSize, - timingRes, - clientLitter, - scripts, - scriptSize, - }; - } - function statusHTML(status) { - if (!status) { - return ` -
- Status -
network:
-
${HTMLNote.BLOCKED}
-
-
-
battery:
-
${HTMLNote.BLOCKED}
-
-
-
available:
-
${HTMLNote.BLOCKED}
-
- `; - } - const { charging, chargingTime, dischargingTime, level, memory, memoryInGigabytes, quota, quotaInGigabytes, downlink, effectiveType, rtt, saveData, downlinkMax, type, stackSize, timingRes, } = status; - const statusHash = hashMini({ - memoryInGigabytes, - quotaInGigabytes, - timingRes, - rtt: rtt === 0 ? 0 : -1, - type, - }); - return ` -
- Status${statusHash} -
network:
-
${isNaN(Number(rtt)) ? HTMLNote.UNSUPPORTED : ` -
rtt: ${rtt}, downlink: ${downlink}${downlinkMax ? `, max: ${downlinkMax}` : ''}
-
effectiveType: ${effectiveType}
-
saveData: ${saveData}${type ? `, type: ${type}` : ''}
- `} -
-
-
-
battery:
-
${!level || isNaN(Number(level)) ? HTMLNote.UNSUPPORTED : ` -
level: ${level * 100}%
-
charging: ${charging}
-
charge time: ${chargingTime === Infinity ? 'discharging' : - chargingTime === 0 ? 'fully charged' : - `${+(chargingTime / 60).toFixed(1)} min.`}
-
discharge time: ${dischargingTime === Infinity ? 'charging' : - `${+(dischargingTime / 60).toFixed(1)} min.`}
- `}
-
-
-
available:
-
- ${quota ? `
storage: ${quotaInGigabytes}GB
[${quota}]
` : ''} - ${memory ? `
memory: ${memoryInGigabytes}GB
[${memory}]
` : ''} - ${timingRes ? `
timing res:
${timingRes.join('
')}
` : ''} -
stack: ${stackSize || HTMLNote.BLOCKED}
-
-
- `; - } - - async function getSVG() { - try { - const timer = createTimer(); - await queueEvent(timer); - let lied = (lieProps['SVGRect.height'] || - lieProps['SVGRect.width'] || - lieProps['SVGRect.x'] || - lieProps['SVGRect.y'] || - lieProps['String.fromCodePoint'] || - lieProps['SVGRectElement.getBBox'] || - lieProps['SVGTextContentElement.getExtentOfChar'] || - lieProps['SVGTextContentElement.getSubStringLength'] || - lieProps['SVGTextContentElement.getComputedTextLength']) || false; - const doc = (PHANTOM_DARKNESS && - PHANTOM_DARKNESS.document && - PHANTOM_DARKNESS.document.body ? PHANTOM_DARKNESS.document : - document); - const divElement = document.createElement('div'); - doc.body.appendChild(divElement); - // patch div - patch(divElement, html ` -
- - - - ${EMOJIS.map((emoji) => { - return `${emoji}`; - }).join('')} - - -
- `); - // SVG - const reduceToObject = (nativeObj) => { - const keys = Object.keys(nativeObj.__proto__); - return keys.reduce((acc, key) => { - const val = nativeObj[key]; - const isMethod = typeof val == 'function'; - return isMethod ? acc : { ...acc, [key]: val }; - }, {}); - }; - const reduceToSum = (nativeObj) => { - const keys = Object.keys(nativeObj.__proto__); - return keys.reduce((acc, key) => { - const val = nativeObj[key]; - return isNaN(val) ? acc : (acc += val); - }, 0); - }; - const getObjectSum = (obj) => !obj ? 0 : Object.keys(obj).reduce((acc, key) => acc += Math.abs(obj[key]), 0); - // SVGRect - const svgBox = doc.getElementById('svgBox'); - const bBox = reduceToObject(svgBox.getBBox()); - // compute SVGRect emojis - const pattern = new Set(); - const svgElems = [...svgBox.getElementsByClassName('svgrect-emoji')]; - await queueEvent(timer); - const emojiSet = svgElems.reduce((emojiSet, el, i) => { - const emoji = EMOJIS[i]; - const dimensions = '' + el.getComputedTextLength(); - if (!pattern.has(dimensions)) { - pattern.add(dimensions); - emojiSet.add(emoji); - } - return emojiSet; - }, new Set()); - // svgRect System Sum - const svgrectSystemSum = 0.00001 * [...pattern].map((x) => { - return x.split(',').reduce((acc, x) => acc += (+x || 0), 0); - }).reduce((acc, x) => acc += x, 0); - // detect failed shift calculation - const svgEmojiEl = svgElems[0]; - const initial = svgEmojiEl.getComputedTextLength(); - svgEmojiEl.classList.add('shift-svg'); - const shifted = svgEmojiEl.getComputedTextLength(); - svgEmojiEl.classList.remove('shift-svg'); - const unshifted = svgEmojiEl.getComputedTextLength(); - if ((initial - shifted) != (unshifted - shifted)) { - lied = true; - documentLie('SVGTextContentElement.getComputedTextLength', 'failed unshift calculation'); - } - const data = { - bBox: getObjectSum(bBox), - extentOfChar: reduceToSum(svgElems[0].getExtentOfChar(EMOJIS[0])), - subStringLength: svgElems[0].getSubStringLength(0, 10), - computedTextLength: svgElems[0].getComputedTextLength(), - emojiSet: [...emojiSet], - svgrectSystemSum, - lied, - }; - doc.body.removeChild(doc.getElementById('svg-container')); - logTestResult({ time: timer.stop(), test: 'svg', passed: true }); - return data; - } - catch (error) { - logTestResult({ test: 'svg', passed: false }); - captureError(error); - return; - } - } - function svgHTML(fp) { - if (!fp.svg) { - return ` -
- SVGRect -
bBox: ${HTMLNote.BLOCKED}
-
char: ${HTMLNote.BLOCKED}
-
subs: ${HTMLNote.BLOCKED}
-
text: ${HTMLNote.BLOCKED}
-
${HTMLNote.BLOCKED}
-
`; - } - const { svg: { $hash, bBox, subStringLength, extentOfChar, computedTextLength, emojiSet, svgrectSystemSum, lied, }, } = fp; - const divisor = 10000; - const helpTitle = `SVGTextContentElement.getComputedTextLength()\nhash: ${hashMini(emojiSet)}\n${emojiSet.map((x, i) => i && (i % 6 == 0) ? `${x}\n` : x).join('')}`; - return ` -
- ${performanceLogger.getLog().svg} - SVGRect${hashSlice($hash)} -
bBox: ${bBox ? (bBox / divisor) : HTMLNote.BLOCKED}
-
char: ${extentOfChar ? (extentOfChar / divisor) : HTMLNote.BLOCKED}
-
subs: ${subStringLength ? (subStringLength / divisor) : HTMLNote.BLOCKED}
-
text: ${computedTextLength ? (computedTextLength / divisor) : HTMLNote.BLOCKED}
-
- ${svgrectSystemSum || HTMLNote.UNSUPPORTED} - ${formatEmojiSet(emojiSet)} -
-
- `; - } - - function getTimezone() { - // inspired by https://arkenfox.github.io/TZP - // https://github.com/vvo/tzdb/blob/master/time-zones-names.json - const cities = [ - 'UTC', - 'GMT', - 'Etc/GMT+0', - 'Etc/GMT+1', - 'Etc/GMT+10', - 'Etc/GMT+11', - 'Etc/GMT+12', - 'Etc/GMT+2', - 'Etc/GMT+3', - 'Etc/GMT+4', - 'Etc/GMT+5', - 'Etc/GMT+6', - 'Etc/GMT+7', - 'Etc/GMT+8', - 'Etc/GMT+9', - 'Etc/GMT-1', - 'Etc/GMT-10', - 'Etc/GMT-11', - 'Etc/GMT-12', - 'Etc/GMT-13', - 'Etc/GMT-14', - 'Etc/GMT-2', - 'Etc/GMT-3', - 'Etc/GMT-4', - 'Etc/GMT-5', - 'Etc/GMT-6', - 'Etc/GMT-7', - 'Etc/GMT-8', - 'Etc/GMT-9', - 'Etc/GMT', - 'Africa/Abidjan', - 'Africa/Accra', - 'Africa/Addis_Ababa', - 'Africa/Algiers', - 'Africa/Asmara', - 'Africa/Bamako', - 'Africa/Bangui', - 'Africa/Banjul', - 'Africa/Bissau', - 'Africa/Blantyre', - 'Africa/Brazzaville', - 'Africa/Bujumbura', - 'Africa/Cairo', - 'Africa/Casablanca', - 'Africa/Ceuta', - 'Africa/Conakry', - 'Africa/Dakar', - 'Africa/Dar_es_Salaam', - 'Africa/Djibouti', - 'Africa/Douala', - 'Africa/El_Aaiun', - 'Africa/Freetown', - 'Africa/Gaborone', - 'Africa/Harare', - 'Africa/Johannesburg', - 'Africa/Juba', - 'Africa/Kampala', - 'Africa/Khartoum', - 'Africa/Kigali', - 'Africa/Kinshasa', - 'Africa/Lagos', - 'Africa/Libreville', - 'Africa/Lome', - 'Africa/Luanda', - 'Africa/Lubumbashi', - 'Africa/Lusaka', - 'Africa/Malabo', - 'Africa/Maputo', - 'Africa/Maseru', - 'Africa/Mbabane', - 'Africa/Mogadishu', - 'Africa/Monrovia', - 'Africa/Nairobi', - 'Africa/Ndjamena', - 'Africa/Niamey', - 'Africa/Nouakchott', - 'Africa/Ouagadougou', - 'Africa/Porto-Novo', - 'Africa/Sao_Tome', - 'Africa/Tripoli', - 'Africa/Tunis', - 'Africa/Windhoek', - 'America/Adak', - 'America/Anchorage', - 'America/Anguilla', - 'America/Antigua', - 'America/Araguaina', - 'America/Argentina/Buenos_Aires', - 'America/Argentina/Catamarca', - 'America/Argentina/Cordoba', - 'America/Argentina/Jujuy', - 'America/Argentina/La_Rioja', - 'America/Argentina/Mendoza', - 'America/Argentina/Rio_Gallegos', - 'America/Argentina/Salta', - 'America/Argentina/San_Juan', - 'America/Argentina/San_Luis', - 'America/Argentina/Tucuman', - 'America/Argentina/Ushuaia', - 'America/Aruba', - 'America/Asuncion', - 'America/Atikokan', - 'America/Bahia', - 'America/Bahia_Banderas', - 'America/Barbados', - 'America/Belem', - 'America/Belize', - 'America/Blanc-Sablon', - 'America/Boa_Vista', - 'America/Bogota', - 'America/Boise', - 'America/Cambridge_Bay', - 'America/Campo_Grande', - 'America/Cancun', - 'America/Caracas', - 'America/Cayenne', - 'America/Cayman', - 'America/Chicago', - 'America/Chihuahua', - 'America/Costa_Rica', - 'America/Creston', - 'America/Cuiaba', - 'America/Curacao', - 'America/Danmarkshavn', - 'America/Dawson', - 'America/Dawson_Creek', - 'America/Denver', - 'America/Detroit', - 'America/Dominica', - 'America/Edmonton', - 'America/Eirunepe', - 'America/El_Salvador', - 'America/Fort_Nelson', - 'America/Fortaleza', - 'America/Glace_Bay', - 'America/Godthab', - 'America/Goose_Bay', - 'America/Grand_Turk', - 'America/Grenada', - 'America/Guadeloupe', - 'America/Guatemala', - 'America/Guayaquil', - 'America/Guyana', - 'America/Halifax', - 'America/Havana', - 'America/Hermosillo', - 'America/Indiana/Indianapolis', - 'America/Indiana/Knox', - 'America/Indiana/Marengo', - 'America/Indiana/Petersburg', - 'America/Indiana/Tell_City', - 'America/Indiana/Vevay', - 'America/Indiana/Vincennes', - 'America/Indiana/Winamac', - 'America/Inuvik', - 'America/Iqaluit', - 'America/Jamaica', - 'America/Juneau', - 'America/Kentucky/Louisville', - 'America/Kentucky/Monticello', - 'America/Kralendijk', - 'America/La_Paz', - 'America/Lima', - 'America/Los_Angeles', - 'America/Lower_Princes', - 'America/Maceio', - 'America/Managua', - 'America/Manaus', - 'America/Marigot', - 'America/Martinique', - 'America/Matamoros', - 'America/Mazatlan', - 'America/Menominee', - 'America/Merida', - 'America/Metlakatla', - 'America/Mexico_City', - 'America/Miquelon', - 'America/Moncton', - 'America/Monterrey', - 'America/Montevideo', - 'America/Montserrat', - 'America/Nassau', - 'America/New_York', - 'America/Nipigon', - 'America/Nome', - 'America/Noronha', - 'America/North_Dakota/Beulah', - 'America/North_Dakota/Center', - 'America/North_Dakota/New_Salem', - 'America/Ojinaga', - 'America/Panama', - 'America/Pangnirtung', - 'America/Paramaribo', - 'America/Phoenix', - 'America/Port-au-Prince', - 'America/Port_of_Spain', - 'America/Porto_Velho', - 'America/Puerto_Rico', - 'America/Punta_Arenas', - 'America/Rainy_River', - 'America/Rankin_Inlet', - 'America/Recife', - 'America/Regina', - 'America/Resolute', - 'America/Rio_Branco', - 'America/Santarem', - 'America/Santiago', - 'America/Santo_Domingo', - 'America/Sao_Paulo', - 'America/Scoresbysund', - 'America/Sitka', - 'America/St_Barthelemy', - 'America/St_Johns', - 'America/St_Kitts', - 'America/St_Lucia', - 'America/St_Thomas', - 'America/St_Vincent', - 'America/Swift_Current', - 'America/Tegucigalpa', - 'America/Thule', - 'America/Thunder_Bay', - 'America/Tijuana', - 'America/Toronto', - 'America/Tortola', - 'America/Vancouver', - 'America/Whitehorse', - 'America/Winnipeg', - 'America/Yakutat', - 'America/Yellowknife', - 'Antarctica/Casey', - 'Antarctica/Davis', - 'Antarctica/DumontDUrville', - 'Antarctica/Macquarie', - 'Antarctica/Mawson', - 'Antarctica/McMurdo', - 'Antarctica/Palmer', - 'Antarctica/Rothera', - 'Antarctica/Syowa', - 'Antarctica/Troll', - 'Antarctica/Vostok', - 'Arctic/Longyearbyen', - 'Asia/Aden', - 'Asia/Almaty', - 'Asia/Amman', - 'Asia/Anadyr', - 'Asia/Aqtau', - 'Asia/Aqtobe', - 'Asia/Ashgabat', - 'Asia/Atyrau', - 'Asia/Baghdad', - 'Asia/Bahrain', - 'Asia/Baku', - 'Asia/Bangkok', - 'Asia/Barnaul', - 'Asia/Beirut', - 'Asia/Bishkek', - 'Asia/Brunei', - 'Asia/Calcutta', - 'Asia/Chita', - 'Asia/Choibalsan', - 'Asia/Colombo', - 'Asia/Damascus', - 'Asia/Dhaka', - 'Asia/Dili', - 'Asia/Dubai', - 'Asia/Dushanbe', - 'Asia/Famagusta', - 'Asia/Gaza', - 'Asia/Hebron', - 'Asia/Ho_Chi_Minh', - 'Asia/Hong_Kong', - 'Asia/Hovd', - 'Asia/Irkutsk', - 'Asia/Jakarta', - 'Asia/Jayapura', - 'Asia/Jerusalem', - 'Asia/Kabul', - 'Asia/Kamchatka', - 'Asia/Karachi', - 'Asia/Kathmandu', - 'Asia/Khandyga', - 'Asia/Kolkata', - 'Asia/Krasnoyarsk', - 'Asia/Kuala_Lumpur', - 'Asia/Kuching', - 'Asia/Kuwait', - 'Asia/Macau', - 'Asia/Magadan', - 'Asia/Makassar', - 'Asia/Manila', - 'Asia/Muscat', - 'Asia/Nicosia', - 'Asia/Novokuznetsk', - 'Asia/Novosibirsk', - 'Asia/Omsk', - 'Asia/Oral', - 'Asia/Phnom_Penh', - 'Asia/Pontianak', - 'Asia/Pyongyang', - 'Asia/Qatar', - 'Asia/Qostanay', - 'Asia/Qyzylorda', - 'Asia/Riyadh', - 'Asia/Sakhalin', - 'Asia/Samarkand', - 'Asia/Seoul', - 'Asia/Shanghai', - 'Asia/Singapore', - 'Asia/Srednekolymsk', - 'Asia/Taipei', - 'Asia/Tashkent', - 'Asia/Tbilisi', - 'Asia/Tehran', - 'Asia/Thimphu', - 'Asia/Tokyo', - 'Asia/Tomsk', - 'Asia/Ulaanbaatar', - 'Asia/Urumqi', - 'Asia/Ust-Nera', - 'Asia/Vientiane', - 'Asia/Vladivostok', - 'Asia/Yakutsk', - 'Asia/Yangon', - 'Asia/Yekaterinburg', - 'Asia/Yerevan', - 'Atlantic/Azores', - 'Atlantic/Bermuda', - 'Atlantic/Canary', - 'Atlantic/Cape_Verde', - 'Atlantic/Faroe', - 'Atlantic/Madeira', - 'Atlantic/Reykjavik', - 'Atlantic/South_Georgia', - 'Atlantic/St_Helena', - 'Atlantic/Stanley', - 'Australia/Adelaide', - 'Australia/Brisbane', - 'Australia/Broken_Hill', - 'Australia/Currie', - 'Australia/Darwin', - 'Australia/Eucla', - 'Australia/Hobart', - 'Australia/Lindeman', - 'Australia/Lord_Howe', - 'Australia/Melbourne', - 'Australia/Perth', - 'Australia/Sydney', - 'Europe/Amsterdam', - 'Europe/Andorra', - 'Europe/Astrakhan', - 'Europe/Athens', - 'Europe/Belgrade', - 'Europe/Berlin', - 'Europe/Bratislava', - 'Europe/Brussels', - 'Europe/Bucharest', - 'Europe/Budapest', - 'Europe/Busingen', - 'Europe/Chisinau', - 'Europe/Copenhagen', - 'Europe/Dublin', - 'Europe/Gibraltar', - 'Europe/Guernsey', - 'Europe/Helsinki', - 'Europe/Isle_of_Man', - 'Europe/Istanbul', - 'Europe/Jersey', - 'Europe/Kaliningrad', - 'Europe/Kiev', - 'Europe/Kirov', - 'Europe/Lisbon', - 'Europe/Ljubljana', - 'Europe/London', - 'Europe/Luxembourg', - 'Europe/Madrid', - 'Europe/Malta', - 'Europe/Mariehamn', - 'Europe/Minsk', - 'Europe/Monaco', - 'Europe/Moscow', - 'Europe/Oslo', - 'Europe/Paris', - 'Europe/Podgorica', - 'Europe/Prague', - 'Europe/Riga', - 'Europe/Rome', - 'Europe/Samara', - 'Europe/San_Marino', - 'Europe/Sarajevo', - 'Europe/Saratov', - 'Europe/Simferopol', - 'Europe/Skopje', - 'Europe/Sofia', - 'Europe/Stockholm', - 'Europe/Tallinn', - 'Europe/Tirane', - 'Europe/Ulyanovsk', - 'Europe/Uzhgorod', - 'Europe/Vaduz', - 'Europe/Vatican', - 'Europe/Vienna', - 'Europe/Vilnius', - 'Europe/Volgograd', - 'Europe/Warsaw', - 'Europe/Zagreb', - 'Europe/Zaporozhye', - 'Europe/Zurich', - 'Indian/Antananarivo', - 'Indian/Chagos', - 'Indian/Christmas', - 'Indian/Cocos', - 'Indian/Comoro', - 'Indian/Kerguelen', - 'Indian/Mahe', - 'Indian/Maldives', - 'Indian/Mauritius', - 'Indian/Mayotte', - 'Indian/Reunion', - 'Pacific/Apia', - 'Pacific/Auckland', - 'Pacific/Bougainville', - 'Pacific/Chatham', - 'Pacific/Chuuk', - 'Pacific/Easter', - 'Pacific/Efate', - 'Pacific/Enderbury', - 'Pacific/Fakaofo', - 'Pacific/Fiji', - 'Pacific/Funafuti', - 'Pacific/Galapagos', - 'Pacific/Gambier', - 'Pacific/Guadalcanal', - 'Pacific/Guam', - 'Pacific/Honolulu', - 'Pacific/Kiritimati', - 'Pacific/Kosrae', - 'Pacific/Kwajalein', - 'Pacific/Majuro', - 'Pacific/Marquesas', - 'Pacific/Midway', - 'Pacific/Nauru', - 'Pacific/Niue', - 'Pacific/Norfolk', - 'Pacific/Noumea', - 'Pacific/Pago_Pago', - 'Pacific/Palau', - 'Pacific/Pitcairn', - 'Pacific/Pohnpei', - 'Pacific/Port_Moresby', - 'Pacific/Rarotonga', - 'Pacific/Saipan', - 'Pacific/Tahiti', - 'Pacific/Tarawa', - 'Pacific/Tongatapu', - 'Pacific/Wake', - 'Pacific/Wallis', - ]; - const getTimezoneOffset = () => { - const [year, month, day] = JSON.stringify(new Date()) - .slice(1, 11) - .split('-'); - const dateString = `${month}/${day}/${year}`; - const dateStringUTC = `${year}-${month}-${day}`; - const now = +new Date(dateString); - const utc = +new Date(dateStringUTC); - const offset = +((now - utc) / 60000); - return ~~offset; - }; - const getTimezoneOffsetHistory = ({ year, city = null }) => { - const format = { - timeZone: '', - year: 'numeric', - month: 'numeric', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }; - const minute = 60000; - let formatter; - let summer; - if (city) { - const options = { - ...format, - timeZone: city, - }; - // @ts-ignore - formatter = new Intl.DateTimeFormat('en', options); - summer = +new Date(formatter.format(new Date(`7/1/${year}`))); - } - else { - summer = +new Date(`7/1/${year}`); - } - const summerUTCTime = +new Date(`${year}-07-01`); - const offset = (summer - summerUTCTime) / minute; - return offset; - }; - const binarySearch = (list, fn) => { - const end = list.length; - const middle = Math.floor(end / 2); - const [left, right] = [list.slice(0, middle), list.slice(middle, end)]; - const found = fn(left); - return end == 1 || found.length ? found : binarySearch(right, fn); - }; - const decryptLocation = ({ year, timeZone }) => { - const system = getTimezoneOffsetHistory({ year }); - const resolvedOptions = getTimezoneOffsetHistory({ year, city: timeZone }); - const filter = (cities) => cities - .filter((city) => system == getTimezoneOffsetHistory({ year, city })); - // get city region set - const decryption = (system == resolvedOptions ? [timeZone] : binarySearch(cities, filter)); - // reduce set to one city - const decrypted = (decryption.length == 1 && decryption[0] == timeZone ? timeZone : hashMini(decryption)); - return decrypted; - }; - const formatLocation = (x) => { - try { - return x.replace(/_/, ' ').split('/').join(', '); - } - catch (error) { } - return x; - }; - try { - const timer = createTimer(); - timer.start(); - const lied = (lieProps['Date.getTimezoneOffset'] || - lieProps['Intl.DateTimeFormat.resolvedOptions'] || - lieProps['Intl.RelativeTimeFormat.resolvedOptions']) || false; - const year = 1113; - // eslint-disable-next-line new-cap - const { timeZone } = Intl.DateTimeFormat().resolvedOptions(); - const decrypted = decryptLocation({ year, timeZone }); - const locationEpoch = +new Date(new Date(`7/1/${year}`)); - const notWithinParentheses = /.*\(|\).*/g; - const data = { - zone: ('' + new Date()).replace(notWithinParentheses, ''), - location: formatLocation(timeZone), - locationMeasured: formatLocation(decrypted), - locationEpoch, - offset: new Date().getTimezoneOffset(), - offsetComputed: getTimezoneOffset(), - lied, - }; - logTestResult({ time: timer.stop(), test: 'timezone', passed: true }); - return { ...data }; - } - catch (error) { - logTestResult({ test: 'timezone', passed: false }); - captureError(error); - return; - } - } - function timezoneHTML(fp) { - if (!fp.timezone) { - return ` -
- Timezone -
${HTMLNote.BLOCKED}
-
`; - } - const { timezone: { $hash, zone, location, locationMeasured, locationEpoch, offset, offsetComputed, lied, }, } = fp; - return ` -
- ${performanceLogger.getLog().timezone} - Timezone${hashSlice($hash)} -
- ${zone ? zone : ''} -
${location != locationMeasured ? locationMeasured : location} -
${locationEpoch} -
${offset != offsetComputed ? offsetComputed : offset} -
-
- `; - } - - async function getCanvasWebgl() { - // use short list to improve performance - const getParamNames = () => [ - // 'BLEND_EQUATION', - // 'BLEND_EQUATION_RGB', - // 'BLEND_EQUATION_ALPHA', - // 'BLEND_DST_RGB', - // 'BLEND_SRC_RGB', - // 'BLEND_DST_ALPHA', - // 'BLEND_SRC_ALPHA', - // 'BLEND_COLOR', - // 'CULL_FACE', - // 'BLEND', - // 'DITHER', - // 'STENCIL_TEST', - // 'DEPTH_TEST', - // 'SCISSOR_TEST', - // 'POLYGON_OFFSET_FILL', - // 'SAMPLE_ALPHA_TO_COVERAGE', - // 'SAMPLE_COVERAGE', - // 'LINE_WIDTH', - 'ALIASED_POINT_SIZE_RANGE', - 'ALIASED_LINE_WIDTH_RANGE', - // 'CULL_FACE_MODE', - // 'FRONT_FACE', - // 'DEPTH_RANGE', - // 'DEPTH_WRITEMASK', - // 'DEPTH_CLEAR_VALUE', - // 'DEPTH_FUNC', - // 'STENCIL_CLEAR_VALUE', - // 'STENCIL_FUNC', - // 'STENCIL_FAIL', - // 'STENCIL_PASS_DEPTH_FAIL', - // 'STENCIL_PASS_DEPTH_PASS', - // 'STENCIL_REF', - 'STENCIL_VALUE_MASK', - 'STENCIL_WRITEMASK', - // 'STENCIL_BACK_FUNC', - // 'STENCIL_BACK_FAIL', - // 'STENCIL_BACK_PASS_DEPTH_FAIL', - // 'STENCIL_BACK_PASS_DEPTH_PASS', - // 'STENCIL_BACK_REF', - 'STENCIL_BACK_VALUE_MASK', - 'STENCIL_BACK_WRITEMASK', - // 'VIEWPORT', - // 'SCISSOR_BOX', - // 'COLOR_CLEAR_VALUE', - // 'COLOR_WRITEMASK', - // 'UNPACK_ALIGNMENT', - // 'PACK_ALIGNMENT', - 'MAX_TEXTURE_SIZE', - 'MAX_VIEWPORT_DIMS', - 'SUBPIXEL_BITS', - // 'RED_BITS', - // 'GREEN_BITS', - // 'BLUE_BITS', - // 'ALPHA_BITS', - // 'DEPTH_BITS', - // 'STENCIL_BITS', - // 'POLYGON_OFFSET_UNITS', - // 'POLYGON_OFFSET_FACTOR', - // 'SAMPLE_BUFFERS', - // 'SAMPLES', - // 'SAMPLE_COVERAGE_VALUE', - // 'SAMPLE_COVERAGE_INVERT', - // 'COMPRESSED_TEXTURE_FORMATS', - // 'GENERATE_MIPMAP_HINT', - 'MAX_VERTEX_ATTRIBS', - 'MAX_VERTEX_UNIFORM_VECTORS', - 'MAX_VARYING_VECTORS', - 'MAX_COMBINED_TEXTURE_IMAGE_UNITS', - 'MAX_VERTEX_TEXTURE_IMAGE_UNITS', - 'MAX_TEXTURE_IMAGE_UNITS', - 'MAX_FRAGMENT_UNIFORM_VECTORS', - 'SHADING_LANGUAGE_VERSION', - 'VENDOR', - 'RENDERER', - 'VERSION', - 'MAX_CUBE_MAP_TEXTURE_SIZE', - // 'ACTIVE_TEXTURE', - // 'IMPLEMENTATION_COLOR_READ_TYPE', - // 'IMPLEMENTATION_COLOR_READ_FORMAT', - 'MAX_RENDERBUFFER_SIZE', - // 'UNPACK_FLIP_Y_WEBGL', - // 'UNPACK_PREMULTIPLY_ALPHA_WEBGL', - // 'UNPACK_COLORSPACE_CONVERSION_WEBGL', - // 'READ_BUFFER', - // 'UNPACK_ROW_LENGTH', - // 'UNPACK_SKIP_ROWS', - // 'UNPACK_SKIP_PIXELS', - // 'PACK_ROW_LENGTH', - // 'PACK_SKIP_ROWS', - // 'PACK_SKIP_PIXELS', - // 'UNPACK_SKIP_IMAGES', - // 'UNPACK_IMAGE_HEIGHT', - 'MAX_3D_TEXTURE_SIZE', - 'MAX_ELEMENTS_VERTICES', - 'MAX_ELEMENTS_INDICES', - 'MAX_TEXTURE_LOD_BIAS', - 'MAX_DRAW_BUFFERS', - // 'DRAW_BUFFER0', - // 'DRAW_BUFFER1', - // 'DRAW_BUFFER2', - // 'DRAW_BUFFER3', - // 'DRAW_BUFFER4', - // 'DRAW_BUFFER5', - // 'DRAW_BUFFER6', - // 'DRAW_BUFFER7', - 'MAX_FRAGMENT_UNIFORM_COMPONENTS', - 'MAX_VERTEX_UNIFORM_COMPONENTS', - // 'FRAGMENT_SHADER_DERIVATIVE_HINT', - 'MAX_ARRAY_TEXTURE_LAYERS', - // 'MIN_PROGRAM_TEXEL_OFFSET', - 'MAX_PROGRAM_TEXEL_OFFSET', - 'MAX_VARYING_COMPONENTS', - 'MAX_TRANSFORM_FEEDBACK_SEPARATE_COMPONENTS', - // 'RASTERIZER_DISCARD', - 'MAX_TRANSFORM_FEEDBACK_INTERLEAVED_COMPONENTS', - 'MAX_TRANSFORM_FEEDBACK_SEPARATE_ATTRIBS', - 'MAX_COLOR_ATTACHMENTS', - 'MAX_SAMPLES', - 'MAX_VERTEX_UNIFORM_BLOCKS', - 'MAX_FRAGMENT_UNIFORM_BLOCKS', - 'MAX_COMBINED_UNIFORM_BLOCKS', - 'MAX_UNIFORM_BUFFER_BINDINGS', - 'MAX_UNIFORM_BLOCK_SIZE', - 'MAX_COMBINED_VERTEX_UNIFORM_COMPONENTS', - 'MAX_COMBINED_FRAGMENT_UNIFORM_COMPONENTS', - // 'UNIFORM_BUFFER_OFFSET_ALIGNMENT', - 'MAX_VERTEX_OUTPUT_COMPONENTS', - 'MAX_FRAGMENT_INPUT_COMPONENTS', - 'MAX_SERVER_WAIT_TIMEOUT', - // 'TRANSFORM_FEEDBACK_PAUSED', - // 'TRANSFORM_FEEDBACK_ACTIVE', - 'MAX_ELEMENT_INDEX', - 'MAX_CLIENT_WAIT_TIMEOUT_WEBGL', - ].sort(); - const draw = (gl) => { - const isSafari15AndAbove = ('BigInt64Array' in window && - IS_WEBKIT && - !/(Cr|Fx)iOS/.test(navigator.userAgent)); - if (!gl || isSafari15AndAbove) { - return; - } - // gl.clearColor(0.47, 0.7, 0.78, 1) - gl.clear(gl.COLOR_BUFFER_BIT); - // based on https://github.com/Valve/fingerprintjs2/blob/master/fingerprint2.js - const vertexPosBuffer = gl.createBuffer(); - gl.bindBuffer(gl.ARRAY_BUFFER, vertexPosBuffer); - const vertices = new Float32Array([-0.9, -0.7, 0, 0.8, -0.7, 0, 0, 0.5, 0]); - gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW); - // create program - const program = gl.createProgram(); - // compile and attach vertex shader - const vertexShader = gl.createShader(gl.VERTEX_SHADER); - gl.shaderSource(vertexShader, ` - attribute vec2 attrVertex; - varying vec2 varyinTexCoordinate; - uniform vec2 uniformOffset; - void main(){ - varyinTexCoordinate = attrVertex + uniformOffset; - gl_Position = vec4(attrVertex, 0, 1); - } - `); - gl.compileShader(vertexShader); - gl.attachShader(program, vertexShader); - // compile and attach fragment shader - const fragmentShader = gl.createShader(gl.FRAGMENT_SHADER); - gl.shaderSource(fragmentShader, ` - precision mediump float; - varying vec2 varyinTexCoordinate; - void main() { - gl_FragColor = vec4(varyinTexCoordinate, 1, 1); - } - `); - gl.compileShader(fragmentShader); - gl.attachShader(program, fragmentShader); - // use program - const componentSize = 3; - gl.linkProgram(program); - gl.useProgram(program); - program.vertexPosAttrib = gl.getAttribLocation(program, 'attrVertex'); - program.offsetUniform = gl.getUniformLocation(program, 'uniformOffset'); - gl.enableVertexAttribArray(program.vertexPosArray); - gl.vertexAttribPointer(program.vertexPosAttrib, componentSize, gl.FLOAT, false, 0, 0); - gl.uniform2f(program.offsetUniform, 1, 1); - // draw - const numOfIndices = 3; - gl.drawArrays(gl.LINE_LOOP, 0, numOfIndices); - return gl; - }; - try { - const timer = createTimer(); - await queueEvent(timer); - // detect lies - const dataLie = lieProps['HTMLCanvasElement.toDataURL']; - const contextLie = lieProps['HTMLCanvasElement.getContext']; - const parameterOrExtensionLie = (lieProps['WebGLRenderingContext.getParameter'] || - lieProps['WebGL2RenderingContext.getParameter'] || - lieProps['WebGLRenderingContext.getExtension'] || - lieProps['WebGL2RenderingContext.getExtension']); - const lied = (dataLie || - contextLie || - parameterOrExtensionLie || - lieProps['WebGLRenderingContext.getSupportedExtensions'] || - lieProps['WebGL2RenderingContext.getSupportedExtensions']) || false; - // create canvas context - let win = window; - if (!LIKE_BRAVE && PHANTOM_DARKNESS) { - win = PHANTOM_DARKNESS; - } - const doc = win.document; - let canvas; - let canvas2; - if ('OffscreenCanvas' in window) { - // @ts-ignore OffscreenCanvas - canvas = new win.OffscreenCanvas(256, 256); - // @ts-ignore OffscreenCanvas - canvas2 = new win.OffscreenCanvas(256, 256); - } - else { - canvas = doc.createElement('canvas'); - canvas2 = doc.createElement('canvas'); - } - const getContext = (canvas, contextType) => { - try { - if (contextType == 'webgl2') { - return (canvas.getContext('webgl2') || - canvas.getContext('experimental-webgl2')); - } - return (canvas.getContext('webgl') || - canvas.getContext('experimental-webgl') || - canvas.getContext('moz-webgl') || - canvas.getContext('webkit-3d')); - } - catch (error) { - return; - } - }; - const gl = getContext(canvas, 'webgl'); - const gl2 = getContext(canvas2, 'webgl2'); - if (!gl) { - logTestResult({ test: 'webgl', passed: false }); - return; - } - // helpers - const getShaderPrecisionFormat = (gl, shaderType) => { - if (!gl) { - return; - } - const LOW_FLOAT = attempt(() => gl.getShaderPrecisionFormat(gl[shaderType], gl.LOW_FLOAT)); - const MEDIUM_FLOAT = attempt(() => gl.getShaderPrecisionFormat(gl[shaderType], gl.MEDIUM_FLOAT)); - const HIGH_FLOAT = attempt(() => gl.getShaderPrecisionFormat(gl[shaderType], gl.HIGH_FLOAT)); - const HIGH_INT = attempt(() => gl.getShaderPrecisionFormat(gl[shaderType], gl.HIGH_INT)); - return { - LOW_FLOAT, - MEDIUM_FLOAT, - HIGH_FLOAT, - HIGH_INT, - }; - }; - const getShaderData = (name, shader) => { - const data = {}; - // eslint-disable-next-line guard-for-in - for (const prop in shader) { - const obj = shader[prop]; - data[name + '.' + prop + '.precision'] = obj ? attempt(() => obj.precision) : undefined; - data[name + '.' + prop + '.rangeMax'] = obj ? attempt(() => obj.rangeMax) : undefined; - data[name + '.' + prop + '.rangeMin'] = obj ? attempt(() => obj.rangeMin) : undefined; - } - return data; - }; - const getMaxAnisotropy = (gl) => { - if (!gl) { - return; - } - const ext = (gl.getExtension('EXT_texture_filter_anisotropic') || - gl.getExtension('MOZ_EXT_texture_filter_anisotropic') || - gl.getExtension('WEBKIT_EXT_texture_filter_anisotropic')); - return ext ? gl.getParameter(ext.MAX_TEXTURE_MAX_ANISOTROPY_EXT) : undefined; - }; - const getParams = (gl) => { - if (!gl) { - return {}; - } - const pnamesShortList = new Set(getParamNames()); - const pnames = Object.getOwnPropertyNames(Object.getPrototypeOf(gl)) - // .filter(prop => prop.toUpperCase() == prop) // global test - .filter((name) => pnamesShortList.has(name)); - return pnames.reduce((acc, name) => { - const val = gl.getParameter(gl[name]); - if (!!val && 'buffer' in Object.getPrototypeOf(val)) { - acc[name] = [...val]; - } - else { - acc[name] = val; - } - return acc; - }, {}); - }; - const getUnmasked = (gl) => { - const ext = !!gl ? gl.getExtension('WEBGL_debug_renderer_info') : null; - return !ext ? {} : { - UNMASKED_VENDOR_WEBGL: gl.getParameter(ext.UNMASKED_VENDOR_WEBGL), - UNMASKED_RENDERER_WEBGL: gl.getParameter(ext.UNMASKED_RENDERER_WEBGL), - }; - }; - const getSupportedExtensions = (gl) => { - if (!gl) { - return []; - } - const ext = attempt(() => gl.getSupportedExtensions()); - if (!ext) { - return []; - } - return ext; - }; - const getWebGLData = (gl, contextType) => { - if (!gl) { - return { - dataURI: undefined, - pixels: undefined, - }; - } - try { - draw(gl); - const { drawingBufferWidth, drawingBufferHeight } = gl; - let dataURI = ''; - if (gl.canvas.constructor.name === 'OffscreenCanvas') { - const canvas = document.createElement('canvas'); - draw(getContext(canvas, contextType)); - dataURI = canvas.toDataURL(); - } - else { - dataURI = gl.canvas.toDataURL(); - } - // reduce excessive reads to improve performance - const width = drawingBufferWidth / 15; - const height = drawingBufferHeight / 6; - const pixels = new Uint8Array(width * height * 4); - try { - gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); - } - catch (error) { - return { - dataURI, - pixels: undefined, - }; - } - // console.log([...pixels].filter(x => !!x)) // test read - return { - dataURI, - pixels: [...pixels], - }; - } - catch (error) { - return captureError(error); - } - }; - // get data - await queueEvent(timer); - const params = { ...getParams(gl), ...getUnmasked(gl) }; - const params2 = { ...getParams(gl2), ...getUnmasked(gl2) }; - const VersionParam = { - ALIASED_LINE_WIDTH_RANGE: true, - SHADING_LANGUAGE_VERSION: true, - VERSION: true, - }; - const mismatch = Object.keys(params2) - .filter((key) => !!params[key] && !VersionParam[key] && ('' + params[key] != '' + params2[key])); - if (mismatch.length) { - sendToTrash('webgl/webgl2 mirrored params mismatch', mismatch.toString()); - } - await queueEvent(timer); - const { dataURI, pixels } = getWebGLData(gl, 'webgl') || {}; - const { dataURI: dataURI2, pixels: pixels2 } = getWebGLData(gl2, 'webgl2') || {}; - const data = { - extensions: [...getSupportedExtensions(gl), ...getSupportedExtensions(gl2)], - pixels, - pixels2, - dataURI, - dataURI2, - parameters: { - ...{ ...params, ...params2 }, - ...{ - antialias: gl.getContextAttributes() ? gl.getContextAttributes().antialias : undefined, - MAX_VIEWPORT_DIMS: attempt(() => [...gl.getParameter(gl.MAX_VIEWPORT_DIMS)]), - MAX_TEXTURE_MAX_ANISOTROPY_EXT: getMaxAnisotropy(gl), - ...getShaderData('VERTEX_SHADER', getShaderPrecisionFormat(gl, 'VERTEX_SHADER')), - ...getShaderData('FRAGMENT_SHADER', getShaderPrecisionFormat(gl, 'FRAGMENT_SHADER')), - MAX_DRAW_BUFFERS_WEBGL: attempt(() => { - const buffers = gl.getExtension('WEBGL_draw_buffers'); - return buffers ? gl.getParameter(buffers.MAX_DRAW_BUFFERS_WEBGL) : undefined; - }), - }, - }, - parameterOrExtensionLie, - lied, - }; - // Firewall - const brandCapabilities = ['00b72507', '00c1b42d', '00fe1ec9', '02b3eea3', '0461d3de', '0463627d', '057857ac', '0586e20b', '0639a81a', '087d5759', '08847ba5', '0b2d4333', '0cdb985d', '0e058699', '0eb2fc19', '0f39d057', '0f840379', '0fc123c7', '101e0582', '12e92e62', '12f8ac14', '1453d59a', '149a1efa', '166dc7c8', '16c481a6', '171831c5', '177cc258', '18579e83', '19594666', '1b251fd7', '1bfd326c', '1e8a9a79', '1ff7c7e7', '2048bc5a', '2259b706', '22d0f2cf', '230d6a0d', '23d1ce20', '2402c3d2', '24306836', '258789d0', '25a760b8', '25f9385d', '27938830', '27db292c', '2b80fd96', '2bb488da', '2c04c2eb', '2d15287f', '2f014c41', '2f582ed9', '300ee927', '33bc5492', '34270469', '3660b71f', '3740c4c7', '3999a5e1', '39ead506', '3a91d0d6', '3b724916', '3bf321b8', '3c546144', '3f9ef44c', '3fea1100', '3ff82303', '4027d193', '402e1064', '4065cd69', '43038e3d', '4503e771', '461f97e1', '464d51ac', '467b99a5', '482c81b2', '48af038f', '4962ada1', '49bf7358', '4c9e8f5d', '502c402c', '508d1625', '52e348ba', '534002ab', '5582debe', '55d3aa56', '55e821f7', '581f3282', '5831d5fd', '58871380', '58fdc720', '5a5658f1', '5a90a5f8', '5aea1af1', '5b6a17aa', '5bef9a39', '5ca55292', '5d786cef', '5ddb9237', '5ee41456', '61178f2a', '61ca8e23', '61d9464e', '61eecaae', '623c3bfd', '6248d9e3', '6294d84e', '62bf7ef1', '6346cf49', '6357365c', '66628310', '668f0f93', '66d992e8', '67995996', '6843ebbf', '6864dcb0', '6951838b', '696e1548', '698c5c2e', '6a75ae3b', '6aa1ff7e', '6b07d4f8', '6b290cd4', '6c168801', '6dfae3cb', '6e806ffc', '6edf1720', '6f81cbe7', '70859bdb', '70a095b1', '7238c5dd', '7360ebd1', '741688e4', '74daf866', '78640859', '79284c47', '794f8929', '795e5c95', '79a57aa9', '7aa13573', '7b2e5242', '7b811cdd', '7ec0ea6b', '801d73af', '802e2547', '81b9cd29', '8219e1a4', '82a9a2f1', '8428fc8e', '849ccb64', '8541aa4c', '85479b99', '8bd0b91b', '8d371161', '903c8847', '917871e7', '98aeaba9', '99b1a1c6', '99ef2c3b', '9b67b7dc', '9c6df98c', '9c814c1b', '9e2b5e94', '9fd76352', 'a1c808d5', 'a22788f8', 'a2383001', 'a26e9aa9', 'a397a568', 'a3f9ee34', 'a4b988da', 'a4d34176', 'a581f55e', 'a5a477ae', 'a9640880', 'a97d3858', 'aa73f3a4', 'ab40bece', 'ac4d4ba8', 'ad01a422', 'ade75c4f', 'ae2c4777', 'afa583bc', 'b10c2a85', 'b224cc7c', 'b2d6fc98', 'b362c2f5', 'b467620a', 'b4d40dcc', 'b504662d', 'b50edd99', 'b5494027', 'b62321c3', 'b8961d15', 'b8ea6e7f', 'bb77a469', 'bc0f9686', 'bcf7315f', 'be2dfaea', 'beffda26', 'bf06317e', 'bf610cdb', 'bfe1c212', 'c00582e9', 'c026469d', 'c04889b1', 'c04b0635', 'c04e374a', 'c05f7596', 'c07307c6', 'c092fdf8', 'c25dd065', 'c2bce496', 'c5e9a883', 'c79634c2', 'c7e37ca0', 'c93b5366', 'c9bc4ffd', 'cba1878b', 'cbeade8c', 'ce2e3d16', 'cefb72ca', 'cf9643e6', 'cfd20274', 'd05a66eb', 'd09c1c07', 'd1e76c89', 'd2172943', 'd2dc2474', 'd498797d', 'd6bf35ad', 'd734ea08', 'd860ff42', 'd8bd9e5a', 'd913dafa', 'd970d345', 'dbdbe7a4', 'dc271c35', 'dcd9a29e', 'dd67b076', 'de793ead', 'ded74044', 'df9daeb6', 'e10339b3', 'e142d1f9', 'e155c47e', 'e15afab0', 'e16bb1bb', 'e316e4c0', 'e3eff92a', 'e4569a5b', 'e574bef6', 'e5962ba3', 'e6464c9f', 'e68b5c4e', 'e796b84e', 'e8694547', 'e965d180', 'e965d541', 'e9bdc904', 'e9dbb8d5', 'ea54d525', 'ea59b343', 'ea7f90ea', 'ea8f5ad0', 'eaa13804', 'eb799d34', 'ec050bb6', 'ec928655', 'eed2e5e1', 'ef8f5db1', 'f0d5a3c7', 'f1077334', 'f221fef5', 'f2293447', 'f33d918e', 'f3c6ea11', 'f51056a1', 'f51cab9a', 'f573bb34', 'f5d19934', 'f7451c92', 'f8e65486', 'f9714b3d', 'fa994f33', 'fafa14c0', 'fc37fe1f', 'fca66520', 'fe0997b6']; - const capabilities = [-1056897629, -1056946782, -1073719331, -1147160399, -1147160553, -1147168724, -1147419751, -1147419753, -1147419775, -1147427826, -1147451883, -1147451901, -1147464169, -1147464177, -1147488144, -1147602934, -1147643759, -1147643872, -1147765274, -1148326739, -1148335070, -1148572354, -1148678631, -1148680509, -1148713259, -1164279890, -1164800191, -1164800478, -1332029332, -133757475, -1342154787, -134823971, -16746546, -1878102921, -1878111124, -1962893370, -1962919974, -1962928178, -2130164162, -2130164382, -2130164388, -2130164546, -2130172573, -2130659912, -2145933648, -2145941977, -2145958228, -2145966414, -2145966441, -2145966529, -2145966535, -2145966545, -2145970658, -2145974343, -2145974380, -2145974489, -2145974596, -2145974598, -2145974612, -2145974637, -2145974657, -2145974729, -2146187766, -2146232338, -2146232480, -2146232503, -2146232590, -2146232723, -2146232724, -2146236588, -2146236703, -2146237020, -2146251619, -2146251641, -2146251681, -2146253671, -2146253693, -2146277218, -2146286438, -2146286463, -2146286583, -2146319268, -2146376065, -2146379955, -2146384003, -2146384011, -2146384027, -2146384034, -2146384120, -2146384281, -2146398568, -2146400384, -2146400556, -2146400620, -2146401928, -2146417027, -2146526795, -2146526934, -2147125544, -2147128275, -2147133747, -2147133749, -2147133760, -2147134974, -2147136328, -2147142429, -2147287810, -2147287811, -2147287820, -2147287834, -2147287835, -2147287854, -2147291718, -2147291820, -2147293058, -2147295768, -2147295822, -2147295823, -2147295849, -2147295857, -2147300019, -2147304193, -2147304219, -2147306321, -2147316382, -2147316383, -2147333118, -2147336998, -2147337003, -2147337012, -2147337022, -2147344686, -2147346747, -2147361652, -2147361731, -2147361769, -2147361774, -2147361775, -2147361778, -2147361792, -2147362760, -2147365698, -2147365730, -2147365759, -2147365760, -2147365827, -2147365863, -2147373914, -2147373984, -2147374032, -2147374080, -2147378041, -2147378146, -2147382130, -2147382221, -2147382251, -2147382270, -2147382272, -2147383246, -2147385825, -2147385849, -2147386292, -2147386326, -2147387335, -2147387364, -2147389930, -2147389937, -2147389951, -2147390461, -2147394188, -2147394251, -2147394484, -2147400057, -2147406798, -2147407643, -2147407821, -2147410938, -2147410941, -2147414733, -2147414956, -2147414987, -2147415037, -2147429201, -2147429223, -2147439020, -2147440422, -2147447111, -2147447122, -2147447126, -2147447137, -2147447149, -2147447157, -2147447161, -2147447163, -2147447873, -2147447892, -2147447896, -2147447928, -2147448592, -2147453701, -2147453767, -2147453768, -2147459031, -2147461169, -2147466956, -2147466972, -2147467172, -2147470173, -2147475351, -2147475352, -638494755, -671082546, -677558160, -999987216, 1099536, 1099644, 1147714426, 1197075, 1229835, 1508998, 1509050, 1610618841, 184555483, 2146590728, 2147305224, 2147361749, 2147440438, 2147475085, 2147479181, 21667, 349912, 351513, 83625, 998804992, 998911268, 999148597, 999156922]; - const webglParams = !data.parameters ? undefined : [ - ...new Set(Object.values(data.parameters) - .filter((val) => val && typeof val != 'string') - .flat() - .map((val) => Number(val))), - ].sort((a, b) => (a - b)); - const gpuBrand = getGpuBrand(data.parameters?.UNMASKED_RENDERER_WEBGL); - const webglParamsStr = '' + webglParams; - const webglBrandCapabilities = !gpuBrand || !webglParamsStr ? undefined : hashMini([gpuBrand, webglParamsStr]); - const webglCapabilities = !webglParams ? undefined : webglParams.reduce((acc, val, i) => acc ^ (+val + i), 0); - Analysis.webglParams = webglParamsStr; - Analysis.webglBrandCapabilities = webglBrandCapabilities; - Analysis.webglCapabilities = webglCapabilities; - const hasSusGpu = webglBrandCapabilities && !brandCapabilities.includes(webglBrandCapabilities); - const hasSusCapabilities = webglCapabilities && !capabilities.includes(webglCapabilities); - if (hasSusGpu) { - LowerEntropy.WEBGL = true; - sendToTrash('WebGLRenderingContext.getParameter', 'suspicious gpu'); - } - if (hasSusCapabilities) { - LowerEntropy.WEBGL = true; - sendToTrash('WebGLRenderingContext.getParameter', 'suspicious capabilities'); - } - logTestResult({ time: timer.stop(), test: 'webgl', passed: true }); - return { - ...data, - gpu: { - ...(getWebGLRendererConfidence((data.parameters || {}).UNMASKED_RENDERER_WEBGL) || {}), - compressedGPU: compressWebGLRenderer((data.parameters || {}).UNMASKED_RENDERER_WEBGL), - }, - }; - } - catch (error) { - logTestResult({ test: 'webgl', passed: false }); - captureError(error); - return; - } - } - function webglHTML(fp) { - if (!fp.canvasWebgl) { - return ` -
- WebGL -
images: ${HTMLNote.BLOCKED}
-
pixels: ${HTMLNote.BLOCKED}
-
params (0): ${HTMLNote.BLOCKED}
-
exts (0): ${HTMLNote.BLOCKED}
-
gpu:
-
${HTMLNote.BLOCKED}
-
-
`; - } - const { canvasWebgl: data } = fp; - const id = 'creep-canvas-webgl'; - const { $hash, dataURI, dataURI2, pixels, pixels2, lied, extensions, parameters, gpu, } = data || {}; - const { parts, warnings, gibbers, confidence, grade: confidenceGrade, compressedGPU, } = gpu || {}; - const paramKeys = parameters ? Object.keys(parameters).sort() : []; - return ` - -
- ${performanceLogger.getLog().webgl} - WebGL${hashSlice($hash)} -
images:${!dataURI ? ' ' + HTMLNote.BLOCKED : `${hashMini(dataURI)}${!dataURI2 || dataURI == dataURI2 ? '' : `${hashMini(dataURI2)}`}`}
-
pixels:${!pixels ? ' ' + HTMLNote.BLOCKED : `${hashSlice(pixels)}${!pixels2 || pixels == pixels2 ? '' : `${hashSlice(pixels2)}`}`}
-
params (${count(paramKeys)}): ${!paramKeys.length ? HTMLNote.BLOCKED : - modal(`${id}-parameters`, paramKeys.map((key) => `${key}: ${parameters[key]}`).join('
'), hashMini(parameters))}
-
exts (${count(extensions)}): ${!extensions.length ? HTMLNote.BLOCKED : - modal(`${id}-extensions`, extensions.sort().join('
'), hashMini(extensions))}
- -
gpu:${confidence ? `confidence: ${confidence}` : ''}
-
-
- ${parameters.UNMASKED_VENDOR_WEBGL ? parameters.UNMASKED_VENDOR_WEBGL : ''} - ${!parameters.UNMASKED_RENDERER_WEBGL ? HTMLNote.BLOCKED : `
${parameters.UNMASKED_RENDERER_WEBGL}`} -
-
- ${!dataURI ? '
' : ``} -
- `; - } - - async function getWebRTCDevices() { - if (!navigator?.mediaDevices?.enumerateDevices) - return null; - return navigator.mediaDevices.enumerateDevices().then((devices) => { - return devices.map((device) => device.kind).sort(); - }); - } - const getExtensions = (sdp) => { - const extensions = (('' + sdp).match(/extmap:\d+ [^\n|\r]+/g) || []) - .map((x) => x.replace(/extmap:[^\s]+ /, '')); - return [...new Set(extensions)].sort(); - }; - const createCounter = () => { - let counter = 0; - return { - increment: () => counter += 1, - getValue: () => counter, - }; - }; - // https://webrtchacks.com/sdp-anatomy/ - // https://tools.ietf.org/id/draft-ietf-rtcweb-sdp-08.html - const constructDescriptions = ({ mediaType, sdp, sdpDescriptors, rtxCounter }) => { - if (!('' + sdpDescriptors)) { - return; - } - return sdpDescriptors.reduce((descriptionAcc, descriptor) => { - const matcher = `(rtpmap|fmtp|rtcp-fb):${descriptor} (.+)`; - const formats = (sdp.match(new RegExp(matcher, 'g')) || []); - if (!('' + formats)) { - return descriptionAcc; - } - const isRtxCodec = ('' + formats).includes(' rtx/'); - if (isRtxCodec) { - if (rtxCounter.getValue()) { - return descriptionAcc; - } - rtxCounter.increment(); - } - const getLineData = (x) => x.replace(/[^\s]+ /, ''); - const description = formats.reduce((acc, x) => { - const rawData = getLineData(x); - const data = rawData.split('/'); - const codec = data[0]; - const description = {}; - if (x.includes('rtpmap')) { - if (mediaType == 'audio') { - description.channels = (+data[2]) || 1; - } - description.mimeType = `${mediaType}/${codec}`; - description.clockRates = [+data[1]]; - return { - ...acc, - ...description, - }; - } - else if (x.includes('rtcp-fb')) { - return { - ...acc, - feedbackSupport: [...(acc.feedbackSupport || []), rawData], - }; - } - else if (isRtxCodec) { - return acc; // no sdpFmtpLine - } - return { ...acc, sdpFmtpLine: [...rawData.split(';')] }; - }, {}); - let shouldMerge = false; - const mergerAcc = descriptionAcc.map((x) => { - shouldMerge = x.mimeType == description.mimeType; - if (shouldMerge) { - if (x.feedbackSupport) { - x.feedbackSupport = [ - ...new Set([...x.feedbackSupport, ...description.feedbackSupport]), - ]; - } - if (x.sdpFmtpLine) { - x.sdpFmtpLine = [ - ...new Set([...x.sdpFmtpLine, ...description.sdpFmtpLine]), - ]; - } - return { - ...x, - clockRates: [ - ...new Set([...x.clockRates, ...description.clockRates]), - ], - }; - } - return x; - }); - if (shouldMerge) { - return mergerAcc; - } - return [...descriptionAcc, description]; - }, []); - }; - const getCapabilities = (sdp) => { - const videoDescriptors = ((/m=video [^\s]+ [^\s]+ ([^\n|\r]+)/.exec(sdp) || [])[1] || '').split(' '); - const audioDescriptors = ((/m=audio [^\s]+ [^\s]+ ([^\n|\r]+)/.exec(sdp) || [])[1] || '').split(' '); - const rtxCounter = createCounter(); - return { - audio: constructDescriptions({ - mediaType: 'audio', - sdp, - sdpDescriptors: audioDescriptors, - rtxCounter, - }), - video: constructDescriptions({ - mediaType: 'video', - sdp, - sdpDescriptors: videoDescriptors, - rtxCounter, - }), - }; - }; - const getIPAddress = (sdp) => { - const blocked = '0.0.0.0'; - const candidateEncoding = /((udp|tcp)\s)((\d|\w)+\s)((\d|\w|(\.|\:))+)(?=\s)/ig; - const connectionLineEncoding = /(c=IN\s)(.+)\s/ig; - const connectionLineIpAddress = ((sdp.match(connectionLineEncoding) || [])[0] || '').trim().split(' ')[2]; - if (connectionLineIpAddress && (connectionLineIpAddress != blocked)) { - return connectionLineIpAddress; - } - const candidateIpAddress = ((sdp.match(candidateEncoding) || [])[0] || '').split(' ')[2]; - return candidateIpAddress && (candidateIpAddress != blocked) ? candidateIpAddress : undefined; - }; - async function getWebRTCData() { - return new Promise(async (resolve) => { - if (!window.RTCPeerConnection) { - return resolve(null); - } - const config = { - iceCandidatePoolSize: 1, - iceServers: [ - { - urls: [ - 'stun:stun4.l.google.com:19302', - 'stun:stun3.l.google.com:19302', - // 'stun:stun2.l.google.com:19302', - // 'stun:stun1.l.google.com:19302', - // 'stun:stun.l.google.com:19302', - ], - }, - ], - }; - const connection = new RTCPeerConnection(config); - connection.createDataChannel(''); - const options = { offerToReceiveAudio: 1, offerToReceiveVideo: 1 }; - const offer = await connection.createOffer(options); - connection.setLocalDescription(offer); - const { sdp } = offer || {}; - const extensions = getExtensions(sdp); - const codecsSdp = getCapabilities(sdp); - let iceCandidate = ''; - let foundation = ''; - const giveUpOnIPAddress = setTimeout(() => { - connection.removeEventListener('icecandidate', computeCandidate); - connection.close(); - if (sdp) { - return resolve({ - codecsSdp, - extensions, - foundation, - iceCandidate, - }); - } - return resolve(null); - }, 3000); - const computeCandidate = (event) => { - const { candidate, foundation: foundationProp } = event.candidate || {}; - if (!candidate) { - return; - } - if (!iceCandidate) { - iceCandidate = candidate; - foundation = (/^candidate:([\w]+)/.exec(candidate) || [])[1] || ''; - } - const { sdp } = connection.localDescription || {}; - const address = getIPAddress(sdp); - if (!address) { - return; - } - const knownInterface = { - 842163049: 'public interface', - 2268587630: 'WireGuard', - }; - connection.removeEventListener('icecandidate', computeCandidate); - clearTimeout(giveUpOnIPAddress); - connection.close(); - return resolve({ - codecsSdp, - extensions, - foundation: knownInterface[foundation] || foundation, - foundationProp, - iceCandidate, - address, - stunConnection: candidate, - }); - }; - connection.addEventListener('icecandidate', computeCandidate); - }); - } - function webrtcHTML(webRTC, mediaDevices) { - if (!webRTC && !mediaDevices) { - return ` -
- WebRTC -
host connection:
-
${HTMLNote.BLOCKED}
-
foundation/ip:
-
${HTMLNote.BLOCKED}
-
-
-
sdp capabilities: ${HTMLNote.BLOCKED}
-
stun connection:
-
${HTMLNote.BLOCKED}
-
devices (0): ${HTMLNote.BLOCKED}
-
${HTMLNote.BLOCKED}
-
- `; - } - const { codecsSdp, extensions, foundation, foundationProp, iceCandidate, address, stunConnection, } = webRTC || {}; - const { audio, video } = codecsSdp || {}; - const id = 'creep-webrtc'; - const webRTCHash = hashMini({ - codecsSdp, - extensions, - foundation, - foundationProp, - address, - mediaDevices, - }); - const deviceMap = { - 'audioinput': 'mic', - 'audiooutput': 'audio', - 'videoinput': 'webcam', - }; - const feedbackId = { - 'ccm fir': 'Codec Control Message Full Intra Request (ccm fir)', - 'goog-remb': 'Google\'s Receiver Estimated Maximum Bitrate (goog-remb)', - 'nack': 'Negative ACKs (nack)', - 'nack pli': 'Picture loss Indication and NACK (nack pli)', - 'transport-cc': 'Transport Wide Congestion Control (transport-cc)', - }; - const replaceIndex = ({ list, index, replacement }) => [ - ...list.slice(0, index), - replacement, - ...list.slice(index + 1), - ]; - const mediaDevicesByType = (mediaDevices || []).reduce((acc, x) => { - const deviceType = deviceMap[x] || x; - if (!acc.includes(deviceType)) { - return (acc = [...acc, deviceType]); - } - else if (!deviceType.includes('dual') && (acc.filter((x) => x == deviceType) || []).length == 1) { - return (acc = replaceIndex({ - list: acc, - index: acc.indexOf(deviceType), - replacement: `dual ${deviceType}`, - })); - } - return (acc = [...acc, deviceType]); - }, []); - const getModalTemplate = (list) => (list || []).map((x) => { - return ` - ${x.mimeType} -
Clock Rates: ${x.clockRates.sort((a, b) => b - a).join(', ')} - ${x.channels > 1 ? `
Channels: ${x.channels}` : ''} - ${x.sdpFmtpLine ? `
Format Specific Parameters:
- ${x.sdpFmtpLine.sort().map((x) => x.replace('=', ': ')).join('
- ')}` : ''} - ${x.feedbackSupport ? `
Feedback Support:
- ${x.feedbackSupport.map((x) => { - return feedbackId[x] || x; - }).sort().join('
- ')}` : ''} - `; - }).join('

'); - return ` -
- WebRTC${webRTCHash} -
host connection:
-
${iceCandidate || HTMLNote.BLOCKED}
-
foundation/ip:
-
-
${foundation ? `type & base ip: ${foundation}` : HTMLNote.UNSUPPORTED}
-
${address ? `ip: ${address}` : HTMLNote.BLOCKED}
-
-
-
-
sdp capabilities: ${!codecsSdp ? HTMLNote.BLOCKED : - modal(`${id}-sdp-capabilities`, getModalTemplate(audio) + - '

' + getModalTemplate(video) + - '

extensions
' + extensions.join('
'), hashMini({ audio, video, extensions }))}
-
stun connection:
-
${stunConnection || HTMLNote.BLOCKED}
-
devices (${count(mediaDevices)}):
-
${!mediaDevices || !mediaDevices.length ? HTMLNote.BLOCKED : - mediaDevicesByType.join(', ')} -
-
- `; - } - - function getWindowFeatures() { - try { - const timer = createTimer(); - timer.start(); - const win = PHANTOM_DARKNESS || window; - let keys = Object.getOwnPropertyNames(win) - .filter((key) => !/_|\d{3,}/.test(key)); // clear out known ddg noise - // if Firefox, remove the 'Event' key and push to end for consistent order - // and disregard keys known to be missing in RFP mode - const firefoxKeyMovedByInspect = 'Event'; - const varyingKeysMissingInRFP = ['PerformanceNavigationTiming', 'Performance']; - if (IS_GECKO) { - const index = keys.indexOf(firefoxKeyMovedByInspect); - if (index != -1) { - keys = keys.slice(0, index).concat(keys.slice(index + 1)); - keys = [...keys, firefoxKeyMovedByInspect]; - } - varyingKeysMissingInRFP.forEach((key) => { - const index = keys.indexOf(key); - if (index != -1) { - keys = keys.slice(0, index).concat(keys.slice(index + 1)); - } - return keys; - }); - } - const moz = keys.filter((key) => (/moz/i).test(key)).length; - const webkit = keys.filter((key) => (/webkit/i).test(key)).length; - const apple = keys.filter((key) => (/apple/i).test(key)).length; - const data = { keys, apple, moz, webkit }; - logTestResult({ time: timer.stop(), test: 'window', passed: true }); - return { ...data }; - } - catch (error) { - logTestResult({ test: 'window', passed: false }); - captureError(error); - return; - } - } - function windowFeaturesHTML(fp) { - if (!fp.windowFeatures) { - return ` -
- Window -
keys (0): ${HTMLNote.BLOCKED}
-
`; - } - const { windowFeatures: { $hash, keys, }, } = fp; - return ` -
- ${performanceLogger.getLog().window} - Window${hashSlice($hash)} -
keys (${count(keys)}): ${keys && keys.length ? modal('creep-iframe-content-window-version', keys.join(', ')) : HTMLNote.BLOCKED}
-
- `; - } - - !async function () { - const scope = await spawnWorker(); - if (scope == 0 /* Scope.WORKER */) { - return; - } - const isBrave = IS_BLINK ? await braveBrowser() : false; - const braveMode = isBrave ? getBraveMode() : {}; - const braveFingerprintingBlocking = isBrave && (braveMode.standard || braveMode.strict); - const fingerprint = async () => { - const timeStart = timer(); - const fingerprintTimeStart = timer(); - // @ts-ignore - const [workerScopeComputed, voicesComputed, offlineAudioContextComputed, canvasWebglComputed, canvas2dComputed, windowFeaturesComputed, htmlElementVersionComputed, cssComputed, cssMediaComputed, screenComputed, mathsComputed, consoleErrorsComputed, timezoneComputed, clientRectsComputed, fontsComputed, mediaComputed, svgComputed, resistanceComputed, intlComputed,] = await Promise.all([ - getBestWorkerScope(), - getVoices(), - getOfflineAudioContext(), - getCanvasWebgl(), - getCanvas2d(), - getWindowFeatures(), - getHTMLElementVersion(), - getCSS(), - getCSSMedia(), - getScreen(), - getMaths(), - getConsoleErrors(), - getTimezone(), - getClientRects(), - getFonts(), - getMedia(), - getSVG(), - getResistance(), - getIntl(), - ]).catch((error) => console.error(error.message)); - const navigatorComputed = await getNavigator(workerScopeComputed) - .catch((error) => console.error(error.message)); - // @ts-ignore - const [headlessComputed, featuresComputed,] = await Promise.all([ - getHeadlessFeatures({ - webgl: canvasWebglComputed, - workerScope: workerScopeComputed, - }), - getEngineFeatures({ - cssComputed, - navigatorComputed, - windowFeaturesComputed, - }), - ]).catch((error) => console.error(error.message)); - // @ts-ignore - const [liesComputed, trashComputed, capturedErrorsComputed,] = await Promise.all([ - getLies(), - getTrash(), - getCapturedErrors(), - ]).catch((error) => console.error(error.message)); - const fingerprintTimeEnd = fingerprintTimeStart(); - console.log(`Fingerprinting complete in ${(fingerprintTimeEnd).toFixed(2)}ms`); - // GPU Prediction - const { parameters: gpuParameter } = canvasWebglComputed || {}; - const reducedGPUParameters = { - ...(braveFingerprintingBlocking ? getBraveUnprotectedParameters(gpuParameter) : - gpuParameter), - RENDERER: undefined, - SHADING_LANGUAGE_VERSION: undefined, - UNMASKED_RENDERER_WEBGL: undefined, - UNMASKED_VENDOR_WEBGL: undefined, - VERSION: undefined, - VENDOR: undefined, - }; - // Hashing - const hashStartTime = timer(); - // @ts-ignore - const [windowHash, headlessHash, htmlHash, cssMediaHash, cssHash, styleHash, styleSystemHash, screenHash, voicesHash, canvas2dHash, canvas2dImageHash, canvas2dPaintHash, canvas2dTextHash, canvas2dEmojiHash, canvasWebglHash, canvasWebglImageHash, canvasWebglParametersHash, pixelsHash, pixels2Hash, mathsHash, consoleErrorsHash, timezoneHash, rectsHash, domRectHash, audioHash, fontsHash, workerHash, mediaHash, mimeTypesHash, navigatorHash, liesHash, trashHash, errorsHash, svgHash, resistanceHash, intlHash, featuresHash, deviceOfTimezoneHash,] = await Promise.all([ - hashify(windowFeaturesComputed), - hashify(headlessComputed), - hashify((htmlElementVersionComputed || {}).keys), - hashify(cssMediaComputed), - hashify(cssComputed), - hashify((cssComputed || {}).computedStyle), - hashify((cssComputed || {}).system), - hashify(screenComputed), - hashify(voicesComputed), - hashify(canvas2dComputed), - hashify((canvas2dComputed || {}).dataURI), - hashify((canvas2dComputed || {}).paintURI), - hashify((canvas2dComputed || {}).textURI), - hashify((canvas2dComputed || {}).emojiURI), - hashify(canvasWebglComputed), - hashify((canvasWebglComputed || {}).dataURI), - hashify(reducedGPUParameters), - ((canvasWebglComputed || {}).pixels || []).length ? hashify(canvasWebglComputed.pixels) : undefined, - ((canvasWebglComputed || {}).pixels2 || []).length ? hashify(canvasWebglComputed.pixels2) : undefined, - hashify((mathsComputed || {}).data), - hashify((consoleErrorsComputed || {}).errors), - hashify(timezoneComputed), - hashify(clientRectsComputed), - hashify([ - (clientRectsComputed || {}).elementBoundingClientRect, - (clientRectsComputed || {}).elementClientRects, - (clientRectsComputed || {}).rangeBoundingClientRect, - (clientRectsComputed || {}).rangeClientRects, - ]), - hashify(offlineAudioContextComputed), - hashify(fontsComputed), - hashify(workerScopeComputed), - hashify(mediaComputed), - hashify((mediaComputed || {}).mimeTypes), - hashify(navigatorComputed), - hashify(liesComputed), - hashify(trashComputed), - hashify(capturedErrorsComputed), - hashify(svgComputed), - hashify(resistanceComputed), - hashify(intlComputed), - hashify(featuresComputed), - hashify((() => { - const { bluetoothAvailability, device, deviceMemory, hardwareConcurrency, maxTouchPoints, oscpu, platform, system, userAgentData, } = navigatorComputed || {}; - const { architecture, bitness, mobile, model, platform: uaPlatform, platformVersion, } = userAgentData || {}; - const { 'any-pointer': anyPointer } = cssMediaComputed?.mediaCSS || {}; - const { colorDepth, pixelDepth, height, width } = screenComputed || {}; - const { location, locationEpoch, zone } = timezoneComputed || {}; - const { deviceMemory: deviceMemoryWorker, hardwareConcurrency: hardwareConcurrencyWorker, gpu, platform: platformWorker, system: systemWorker, timezoneLocation: locationWorker, userAgentData: userAgentDataWorker, } = workerScopeComputed || {}; - const { compressedGPU, confidence } = gpu || {}; - const { architecture: architectureWorker, bitness: bitnessWorker, mobile: mobileWorker, model: modelWorker, platform: uaPlatformWorker, platformVersion: platformVersionWorker, } = userAgentDataWorker || {}; - return [ - anyPointer, - architecture, - architectureWorker, - bitness, - bitnessWorker, - bluetoothAvailability, - colorDepth, - ...(compressedGPU && confidence != 'low' ? [compressedGPU] : []), - device, - deviceMemory, - deviceMemoryWorker, - hardwareConcurrency, - hardwareConcurrencyWorker, - height, - location, - locationWorker, - locationEpoch, - maxTouchPoints, - mobile, - mobileWorker, - model, - modelWorker, - oscpu, - pixelDepth, - platform, - platformWorker, - platformVersion, - platformVersionWorker, - system, - systemWorker, - uaPlatform, - uaPlatformWorker, - width, - zone, - ]; - })()), - ]).catch((error) => console.error(error.message)); - // console.log(performance.now()-start) - const hashTimeEnd = hashStartTime(); - const timeEnd = timeStart(); - console.log(`Hashing complete in ${(hashTimeEnd).toFixed(2)}ms`); - if (PARENT_PHANTOM) { - // @ts-ignore - PARENT_PHANTOM.parentNode.removeChild(PARENT_PHANTOM); - } - const fingerprint = { - workerScope: !workerScopeComputed ? undefined : { ...workerScopeComputed, $hash: workerHash }, - navigator: !navigatorComputed ? undefined : { ...navigatorComputed, $hash: navigatorHash }, - windowFeatures: !windowFeaturesComputed ? undefined : { ...windowFeaturesComputed, $hash: windowHash }, - headless: !headlessComputed ? undefined : { ...headlessComputed, $hash: headlessHash }, - htmlElementVersion: !htmlElementVersionComputed ? undefined : { ...htmlElementVersionComputed, $hash: htmlHash }, - cssMedia: !cssMediaComputed ? undefined : { ...cssMediaComputed, $hash: cssMediaHash }, - css: !cssComputed ? undefined : { ...cssComputed, $hash: cssHash }, - screen: !screenComputed ? undefined : { ...screenComputed, $hash: screenHash }, - voices: !voicesComputed ? undefined : { ...voicesComputed, $hash: voicesHash }, - media: !mediaComputed ? undefined : { ...mediaComputed, $hash: mediaHash }, - canvas2d: !canvas2dComputed ? undefined : { ...canvas2dComputed, $hash: canvas2dHash }, - canvasWebgl: !canvasWebglComputed ? undefined : { ...canvasWebglComputed, pixels: pixelsHash, pixels2: pixels2Hash, $hash: canvasWebglHash }, - maths: !mathsComputed ? undefined : { ...mathsComputed, $hash: mathsHash }, - consoleErrors: !consoleErrorsComputed ? undefined : { ...consoleErrorsComputed, $hash: consoleErrorsHash }, - timezone: !timezoneComputed ? undefined : { ...timezoneComputed, $hash: timezoneHash }, - clientRects: !clientRectsComputed ? undefined : { ...clientRectsComputed, $hash: rectsHash }, - offlineAudioContext: !offlineAudioContextComputed ? undefined : { ...offlineAudioContextComputed, $hash: audioHash }, - fonts: !fontsComputed ? undefined : { ...fontsComputed, $hash: fontsHash }, - lies: !liesComputed ? undefined : { ...liesComputed, $hash: liesHash }, - trash: !trashComputed ? undefined : { ...trashComputed, $hash: trashHash }, - capturedErrors: !capturedErrorsComputed ? undefined : { ...capturedErrorsComputed, $hash: errorsHash }, - svg: !svgComputed ? undefined : { ...svgComputed, $hash: svgHash }, - resistance: !resistanceComputed ? undefined : { ...resistanceComputed, $hash: resistanceHash }, - intl: !intlComputed ? undefined : { ...intlComputed, $hash: intlHash }, - features: !featuresComputed ? undefined : { ...featuresComputed, $hash: featuresHash }, - }; - return { - fingerprint, - styleSystemHash, - styleHash, - domRectHash, - mimeTypesHash, - canvas2dImageHash, - canvasWebglImageHash, - canvas2dPaintHash, - canvas2dTextHash, - canvas2dEmojiHash, - canvasWebglParametersHash, - deviceOfTimezoneHash, - timeEnd, - }; - }; - // fingerprint and render - const [{ fingerprint: fp, styleSystemHash, styleHash, domRectHash, mimeTypesHash, canvas2dImageHash, canvas2dPaintHash, canvas2dTextHash, canvas2dEmojiHash, canvasWebglImageHash, canvasWebglParametersHash, deviceOfTimezoneHash, timeEnd, },] = await Promise.all([ - fingerprint().catch((error) => console.error(error)) || {}, - ]); - if (!fp) { - throw new Error('Fingerprint failed!'); - } - console.log('%c✔ loose fingerprint passed', 'color:#4cca9f'); - console.groupCollapsed('Loose Fingerprint'); - console.log(fp); - console.groupEnd(); - console.groupCollapsed('Loose Fingerprint JSON'); - console.log('diff check at https://www.diffchecker.com/diff\n\n', JSON.stringify(fp, null, '\t')); - console.groupEnd(); - const hardenEntropy = (workerScope, prop) => { - return (!workerScope ? prop : - (workerScope.localeEntropyIsTrusty && workerScope.localeIntlEntropyIsTrusty) ? prop : - undefined); - }; - const privacyResistFingerprinting = (fp.resistance && /^(tor browser|firefox)$/i.test(fp.resistance.privacy)); - // harden gpu - const hardenGPU = (canvasWebgl) => { - const { gpu: { confidence, compressedGPU } } = canvasWebgl; - return (confidence == 'low' ? {} : { - UNMASKED_RENDERER_WEBGL: compressedGPU, - UNMASKED_VENDOR_WEBGL: canvasWebgl.parameters.UNMASKED_VENDOR_WEBGL, - }); - }; - const creep = { - navigator: (!fp.navigator || fp.navigator.lied ? undefined : { - bluetoothAvailability: fp.navigator.bluetoothAvailability, - device: fp.navigator.device, - deviceMemory: fp.navigator.deviceMemory, - hardwareConcurrency: fp.navigator.hardwareConcurrency, - maxTouchPoints: fp.navigator.maxTouchPoints, - oscpu: fp.navigator.oscpu, - platform: fp.navigator.platform, - system: fp.navigator.system, - userAgentData: { - ...(fp.navigator.userAgentData || {}), - // loose - brandsVersion: undefined, - uaFullVersion: undefined, - }, - vendor: fp.navigator.vendor, - }), - screen: (!fp.screen || fp.screen.lied || privacyResistFingerprinting || LowerEntropy.SCREEN ? undefined : - hardenEntropy(fp.workerScope, { - height: fp.screen.height, - width: fp.screen.width, - pixelDepth: fp.screen.pixelDepth, - colorDepth: fp.screen.colorDepth, - lied: fp.screen.lied, - })), - workerScope: !fp.workerScope || fp.workerScope.lied ? undefined : { - deviceMemory: (braveFingerprintingBlocking ? undefined : fp.workerScope.deviceMemory), - hardwareConcurrency: (braveFingerprintingBlocking ? undefined : fp.workerScope.hardwareConcurrency), - // system locale in blink - language: !LowerEntropy.TIME_ZONE ? fp.workerScope.language : undefined, - platform: fp.workerScope.platform, - system: fp.workerScope.system, - device: fp.workerScope.device, - timezoneLocation: (!LowerEntropy.TIME_ZONE ? - hardenEntropy(fp.workerScope, fp.workerScope.timezoneLocation) : - undefined), - webglRenderer: ((fp.workerScope.gpu.confidence != 'low') ? fp.workerScope.gpu.compressedGPU : undefined), - webglVendor: ((fp.workerScope.gpu.confidence != 'low') ? fp.workerScope.webglVendor : undefined), - userAgentData: { - ...fp.workerScope.userAgentData, - // loose - brandsVersion: undefined, - uaFullVersion: undefined, - }, - }, - media: fp.media, - canvas2d: ((canvas2d) => { - if (!canvas2d) { - return; - } - const { lied, liedTextMetrics } = canvas2d; - let data; - if (!lied) { - const { dataURI, paintURI, textURI, emojiURI } = canvas2d; - data = { - lied, - ...{ dataURI, paintURI, textURI, emojiURI }, - }; - } - if (!liedTextMetrics) { - const { textMetricsSystemSum, emojiSet } = canvas2d; - data = { - ...(data || {}), - ...{ textMetricsSystemSum, emojiSet }, - }; - } - return data; - })(fp.canvas2d), - canvasWebgl: (!fp.canvasWebgl || fp.canvasWebgl.lied || LowerEntropy.WEBGL) ? undefined : (braveFingerprintingBlocking ? { - parameters: { - ...getBraveUnprotectedParameters(fp.canvasWebgl.parameters), - ...hardenGPU(fp.canvasWebgl), - }, - } : { - ...((gl, canvas2d) => { - if ((canvas2d && canvas2d.lied) || LowerEntropy.CANVAS) { - // distrust images - const { extensions, gpu, lied, parameterOrExtensionLie } = gl; - return { - extensions, - gpu, - lied, - parameterOrExtensionLie, - }; - } - return gl; - })(fp.canvasWebgl, fp.canvas2d), - parameters: { - ...fp.canvasWebgl.parameters, - ...hardenGPU(fp.canvasWebgl), - }, - }), - cssMedia: !fp.cssMedia ? undefined : { - reducedMotion: caniuse(() => fp.cssMedia.mediaCSS['prefers-reduced-motion']), - colorScheme: (braveFingerprintingBlocking ? undefined : - caniuse(() => fp.cssMedia.mediaCSS['prefers-color-scheme'])), - monochrome: caniuse(() => fp.cssMedia.mediaCSS.monochrome), - invertedColors: caniuse(() => fp.cssMedia.mediaCSS['inverted-colors']), - forcedColors: caniuse(() => fp.cssMedia.mediaCSS['forced-colors']), - anyHover: caniuse(() => fp.cssMedia.mediaCSS['any-hover']), - hover: caniuse(() => fp.cssMedia.mediaCSS.hover), - anyPointer: caniuse(() => fp.cssMedia.mediaCSS['any-pointer']), - pointer: caniuse(() => fp.cssMedia.mediaCSS.pointer), - colorGamut: caniuse(() => fp.cssMedia.mediaCSS['color-gamut']), - screenQuery: (privacyResistFingerprinting || (LowerEntropy.SCREEN || LowerEntropy.IFRAME_SCREEN) ? - undefined : - hardenEntropy(fp.workerScope, caniuse(() => fp.cssMedia.screenQuery))), - }, - css: !fp.css ? undefined : fp.css.system.fonts, - timezone: !fp.timezone || fp.timezone.lied || LowerEntropy.TIME_ZONE ? undefined : { - locationMeasured: hardenEntropy(fp.workerScope, fp.timezone.locationMeasured), - lied: fp.timezone.lied, - }, - offlineAudioContext: !fp.offlineAudioContext ? undefined : (fp.offlineAudioContext.lied || LowerEntropy.AUDIO ? undefined : - fp.offlineAudioContext), - fonts: !fp.fonts || fp.fonts.lied || LowerEntropy.FONTS ? undefined : fp.fonts.fontFaceLoadFonts, - forceRenew: 1737085481442, - }; - console.log('%c✔ stable fingerprint passed', 'color:#4cca9f'); - console.groupCollapsed('Stable Fingerprint'); - console.log(creep); - console.groupEnd(); - console.groupCollapsed('Stable Fingerprint JSON'); - console.log('diff check at https://www.diffchecker.com/diff\n\n', JSON.stringify(creep, null, '\t')); - console.groupEnd(); - const [fpHash, creepHash] = await Promise.all([hashify(fp), hashify(creep)]).catch((error) => { - console.error(error.message); - }) || []; - const blankFingerprint = '0000000000000000000000000000000000000000000000000000000000000000'; - const el = document.getElementById('fingerprint-data'); - patch(el, html ` -
-
-
-
FP ID: Computing...
-
-
Fuzzy: ${blankFingerprint}
-
-
${(timeEnd || 0).toFixed(2)} ms
-
-
-
-
- WebRTC -
host connection:
-
- candidate:0000000000 1 udp 9353978903 93549af7-47d4-485c-a57a-751a3d213876.local 56518 typ host generation 0 ufrag bk84 network-cost 999 -
-
foundation/ip:
-
-
0000000000
-
000.000.000.000
-
-
-
-
capabilities:
-
stun connection:
-
- candidate:0000000000 1 udp 9353978903 93549af7-47d4-485c-a57a-751a3d213876.local 56518 typ host generation 0 ufrag bk84 network-cost 999 -
-
devices (0):
-
mic, audio, webcam
-
-
-
- ${timezoneHTML(fp)} - ${intlHTML(fp)} -
-
- ${headlessFeaturesHTML(fp)} - ${resistanceHTML(fp)} -
-
${workerScopeHTML(fp)}
-
- ${webglHTML(fp)} - ${screenHTML(fp)} -
-
- ${canvasHTML(fp)} - ${fontsHTML(fp)} -
-
- ${clientRectsHTML(fp)} - ${svgHTML(fp)} -
-
- ${audioHTML(fp)} - ${voicesHTML(fp)} - ${mediaHTML(fp)} -
-
${featuresHTML(fp)}
-
- ${cssMediaHTML(fp)} - ${cssHTML(fp)} -
-
-
- ${mathsHTML(fp)} - ${consoleErrorsHTML(fp)} -
-
- ${windowFeaturesHTML(fp)} - ${htmlElementVersionHTML(fp)} -
-
-
${navigatorHTML(fp)}
-
-
- Status -
network:
-
-
-
-
battery:
-
-
-
-
available:
-
-
-
- -
- `, async () => { - // send analysis fingerprint - Promise.all([ - getWebRTCData(), - getWebRTCDevices(), - getStatus(), - ]).then(async (data) => { - const [webRTC, mediaDevices, status] = data || []; - patch(document.getElementById('webrtc-connection'), html ` -
- ${webrtcHTML(webRTC, mediaDevices)} -
- `); - patch(document.getElementById('status-info'), html ` -
- ${statusHTML(status)} -
- `); - }).catch((err) => console.error(err)); - // expose results to the window - // @ts-expect-error does not exist - window.Fingerprint = JSON.parse(JSON.stringify(fp)); - // @ts-expect-error does not exist - window.Creep = JSON.parse(JSON.stringify(creep)); - const fuzzyFingerprint = await getFuzzyHash(fp); - const fuzzyFpEl = document.getElementById('fuzzy-fingerprint'); - patch(fuzzyFpEl, html ` -
-
Fuzzy: ${fuzzyFingerprint}
-
- `); - // Display fingerprint - const rand = (min, max) => Math.floor(Math.random() * (max - min + 1) + min); - setTimeout(() => { - patch(document.getElementById('creep-fingerprint'), html ` -
FP ID: ${creepHash?.split('').map((x, i) => { - return `${x}`; - }).join('')}
- `); - }, 50); - }); - }(); - -})(); diff --git a/tests/vendor/fingerprintjs-5.2.0.umd.min.js b/tests/vendor/fingerprintjs-5.2.0.umd.min.js deleted file mode 100644 index 9975db8..0000000 --- a/tests/vendor/fingerprintjs-5.2.0.umd.min.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * FingerprintJS v5.2.0 - Copyright (c) FingerprintJS, Inc, 2026 (https://fingerprint.com) - * - * Licensed under MIT License - * - * Copyright (c) 2025 FingerprintJS, Inc - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).FingerprintJS={})}(this,(function(t){"use strict";var e="5.2.0";function n(t,e){return new Promise((n=>setTimeout(n,t,e)))}function o(t){return!!t&&"function"==typeof t.then}function i(t,e){try{const n=t();o(n)?n.then((t=>e(!0,t)),(t=>e(!1,t))):e(!0,n)}catch(n){e(!1,n)}}async function r(t,e,n=16){const o=Array(t.length);let i=Date.now();for(let r=0;r=i+n&&(i=a,await new Promise((t=>{const e=new MessageChannel;e.port1.onmessage=()=>t(),e.port2.postMessage(null)})))}return o}function a(t){return t.then(void 0,(()=>{})),t}function c(t){return parseInt(t)}function s(t){return parseFloat(t)}function u(t,e){return"number"==typeof t&&isNaN(t)?e:t}function l(t){return t.reduce(((t,e)=>t+(e?1:0)),0)}function d(t,e=1){if(Math.abs(e)>=1)return Math.round(t/e)*e;{const n=1/e;return Math.round(t*n)/n}}function m(t,e){const n=t[0]>>>16,o=65535&t[0],i=t[1]>>>16,r=65535&t[1],a=e[0]>>>16,c=65535&e[0],s=e[1]>>>16;let u=0,l=0,d=0,m=0;m+=r+(65535&e[1]),d+=m>>>16,m&=65535,d+=i+s,l+=d>>>16,d&=65535,l+=o+c,u+=l>>>16,l&=65535,u+=n+a,u&=65535,t[0]=u<<16|l,t[1]=d<<16|m}function f(t,e){const n=t[0]>>>16,o=65535&t[0],i=t[1]>>>16,r=65535&t[1],a=e[0]>>>16,c=65535&e[0],s=e[1]>>>16,u=65535&e[1];let l=0,d=0,m=0,f=0;f+=r*u,m+=f>>>16,f&=65535,m+=i*u,d+=m>>>16,m&=65535,m+=r*s,d+=m>>>16,m&=65535,d+=o*u,l+=d>>>16,d&=65535,d+=i*s,l+=d>>>16,d&=65535,d+=r*c,l+=d>>>16,d&=65535,l+=n*u+o*s+i*c+r*a,l&=65535,t[0]=l<<16|d,t[1]=m<<16|f}function p(t,e){const n=t[0];32===(e%=64)?(t[0]=t[1],t[1]=n):e<32?(t[0]=n<>>32-e,t[1]=t[1]<>>32-e):(e-=32,t[0]=t[1]<>>32-e,t[1]=n<>>32-e)}function h(t,e){0!==(e%=64)&&(e<32?(t[0]=t[1]>>>32-e,t[1]=t[1]<>>1];b(t,e),f(t,y),e[1]=t[0]>>>1,b(t,e),f(t,g),e[1]=t[0]>>>1,b(t,e)}const w=[2277735313,289559509],L=[1291169091,658871167],k=[0,5],V=[0,1390208809],S=[0,944331445];function W(t,e){const n=function(t){const e=new Uint8Array(t.length);for(let n=0;n127)return(new TextEncoder).encode(t);e[n]=o}return e}(t);e=e||0;const o=[0,n.length],i=o[1]%16,r=o[1]-i,a=[0,e],c=[0,e],s=[0,0],u=[0,0];let l;for(l=0;l>>0).toString(16)).slice(-8)+("00000000"+(a[1]>>>0).toString(16)).slice(-8)+("00000000"+(c[0]>>>0).toString(16)).slice(-8)+("00000000"+(c[1]>>>0).toString(16)).slice(-8)}function x(t){return"function"!=typeof t}function Z(t,e,n,o){const c=Object.keys(t).filter((t=>!function(t,e){for(let n=0,o=t.length;nfunction(t,e){const n=a(new Promise((n=>{const o=Date.now();i(t.bind(null,e),((...t)=>{const e=Date.now()-o;if(!t[0])return n((()=>({error:t[1],duration:e})));const r=t[1];if(x(r))return n((()=>({value:r,duration:e})));n((()=>new Promise((t=>{const n=Date.now();i(r,((...o)=>{const i=e+Date.now()-n;if(!o[0])return t({error:o[1],duration:i});t({value:o[1],duration:i})}))}))))}))})));return function(){return n.then((t=>t()))}}(t[n],e)),o));return async function(){const t=await s,e=await r(t,(t=>a(t())),o),n=await Promise.all(e),i={};for(let o=0;o=4}function R(){const t=window,e=navigator;return l(["msWriteProfilerMark"in t,"MSStream"in t,"msLaunchUri"in e,"msSaveBlob"in e])>=3&&!M()}function F(){const t=window,e=navigator;return l(["webkitPersistentStorage"in e,"webkitTemporaryStorage"in e,0===(e.vendor||"").indexOf("Google"),"webkitResolveLocalFileSystemURL"in t,"BatteryManager"in t,"webkitMediaStream"in t,"webkitSpeechGrammar"in t])>=5}function G(){const t=window;return l(["ApplePayError"in t,"CSSPrimitiveValue"in t,"Counter"in t,0===navigator.vendor.indexOf("Apple"),"RGBColor"in t,"WebKitMediaKeys"in t])>=4}function I(){const t=window,{HTMLElement:e,Document:n}=t;return l(["safari"in t,!("ongestureend"in t),!("TouchEvent"in t),!("orientation"in t),e&&!("autocapitalize"in e.prototype),n&&"pointerLockElement"in n.prototype])>=4}function C(){const t=window;return e=t.print,/^function\s.*?\{\s*\[native code]\s*}$/.test(String(e))&&"[object WebPageNamespace]"===String(t.browser);var e}function Y(){var t,e;const n=window;return l(["buildID"in navigator,"MozAppearance"in(null!==(e=null===(t=document.documentElement)||void 0===t?void 0:t.style)&&void 0!==e?e:{}),"onmozfullscreenchange"in n,"mozInnerScreenX"in n,"CSSMozDocumentRule"in n,"CanvasCaptureMediaStream"in n])>=4}function P(){const{CSS:t}=window;return l([t.supports("selector(::details-content)"),t.supports("selector(::before::marker)"),t.supports("selector(::after::marker)"),!("locale"in CompositionEvent.prototype)])>=3}function X(){const t=window,e=document,{CSS:n,Promise:o,AudioContext:i}=t;return l([o&&"try"in o,"caretPositionFromPoint"in e,i&&"onerror"in i.prototype,n.supports("ruby-align","space-around")])>=3}function j(){const t=window,e=navigator,{CSS:n,HTMLButtonElement:o}=t;return l([!("getStorageUpdates"in e),o&&"popover"in o.prototype,"CSSCounterStyleRule"in t,n.supports("font-size-adjust: ex-height 0.5"),n.supports("text-transform: full-width")])>=4}function E(){const t=document;return t.fullscreenElement||t.msFullscreenElement||t.mozFullScreenElement||t.webkitFullscreenElement||null}function H(){const t=F(),e=Y(),n=window,o=navigator,i="connection";return t?l([!("SharedWorker"in n),o[i]&&"ontypechange"in o[i],!("sinkId"in new Audio)])>=2:!!e&&l(["onorientationchange"in n,"orientation"in n,/android/i.test(o.appVersion)])>=2}function A(){const t=navigator,e=window,n=Audio.prototype,{visualViewport:o}=e;return l(["srLatency"in n,"srChannelCount"in n,"devicePosture"in t,o&&"segments"in o,"getTextInformation"in Image.prototype])>=3}function N(){const t=window,e=t.OfflineAudioContext||t.webkitOfflineAudioContext;if(!e)return-2;if(G()&&!I()&&!function(){const t=window;return l(["DOMRectList"in t,"RTCPeerConnectionIceEvent"in t,"SVGGeometryElement"in t,"ontransitioncancel"in t])>=3}())return-1;const n=new e(1,5e3,44100),i=n.createOscillator();i.type="triangle",i.frequency.value=1e4;const r=n.createDynamicsCompressor();r.threshold.value=-50,r.knee.value=40,r.ratio.value=12,r.attack.value=0,r.release.value=.25,i.connect(r),r.connect(n.destination),i.start(0);const[c,s]=function(t){const e=3,n=500,i=500,r=5e3;let c=()=>{};const s=new Promise(((s,u)=>{let l=!1,d=0,m=0;t.oncomplete=t=>s(t.renderedBuffer);const f=()=>{setTimeout((()=>u(J("timeout"))),Math.min(i,m+r-Date.now()))},p=()=>{try{const i=t.startRendering();switch(o(i)&&a(i),t.state){case"running":m=Date.now(),l&&f();break;case"suspended":document.hidden||d++,l&&d>=e?u(J("suspended")):setTimeout(p,n)}}catch(i){u(i)}};p(),c=()=>{l||(l=!0,m>0&&f())}}));return[s,c]}(n),u=a(c.then((t=>function(t){let e=0;for(let n=0;n{if("timeout"===t.name||"suspended"===t.name)return-3;throw t})));return()=>(s(),u)}function J(t){const e=new Error(t);return e.name=t,e}async function T(t,e,o=50){var i,r,a;const c=document;for(;!c.body;)await n(o);const s=c.createElement("iframe");try{for((await new Promise(((t,n)=>{let o=!1;const i=()=>{o=!0,t()};s.onload=i,s.onerror=t=>{o=!0,n(t)};const{style:r}=s;r.setProperty("display","block","important"),r.position="absolute",r.top="0",r.left="0",r.visibility="hidden",e&&"srcdoc"in s?s.srcdoc=e:s.src="about:blank",c.body.appendChild(s);const a=()=>{var t,e;o||("complete"===(null===(e=null===(t=s.contentWindow)||void 0===t?void 0:t.document)||void 0===e?void 0:e.readyState)?i():setTimeout(a,10))};a()})));!(null===(r=null===(i=s.contentWindow)||void 0===i?void 0:i.document)||void 0===r?void 0:r.body);)await n(o);return await t(s,s.contentWindow)}finally{null===(a=s.parentNode)||void 0===a||a.removeChild(s)}}function _(t){const[e,n]=function(t){var e,n;const o=`Unexpected syntax '${t}'`,i=/^\s*([a-z-]*)(.*)$/i.exec(t),r=i[1]||void 0,a={},c=/([.:#][\w-]+|\[.+?\])/gi,s=(t,e)=>{a[t]=a[t]||[],a[t].push(e)};for(;;){const t=c.exec(i[2]);if(!t)break;const r=t[0];switch(r[0]){case".":s("class",r.slice(1));break;case"#":s("id",r.slice(1));break;case"[":{const t=/^\[([\w-]+)([~|^$*]?=("(.*?)"|([\w-]+)))?(\s+[is])?\]$/.exec(r);if(!t)throw new Error(o);s(t[1],null!==(n=null!==(e=t[4])&&void 0!==e?e:t[5])&&void 0!==n?n:"");break}default:throw new Error(o)}}return[r,a]}(t),o=document.createElement(null!=e?e:"div");for(const i of Object.keys(n)){const t=n[i].join(" ");"style"===i?D(o.style,t):o.setAttribute(i,t)}return o}function D(t,e){for(const n of e.split(";")){const e=/^\s*([\w-]+)\s*:\s*(.+?)(\s*!([\w-]+))?\s*$/.exec(n);if(e){const[,n,o,,i]=e;t.setProperty(n,o,i||"")}}}const z=["monospace","sans-serif","serif"],B=["sans-serif-thin","ARNO PRO","Agency FB","Arabic Typesetting","Arial Unicode MS","AvantGarde Bk BT","BankGothic Md BT","Batang","Bitstream Vera Sans Mono","Calibri","Century","Century Gothic","Clarendon","EUROSTILE","Franklin Gothic","Futura Bk BT","Futura Md BT","GOTHAM","Gill Sans","HELV","Haettenschweiler","Helvetica Neue","Humanst521 BT","Leelawadee","Letter Gothic","Levenim MT","Lucida Bright","Lucida Sans","Menlo","MS Mincho","MS Outlook","MS Reference Specialty","MS UI Gothic","MT Extra","MYRIAD PRO","Marlett","Meiryo UI","Microsoft Uighur","Minion Pro","Monotype Corsiva","PMingLiU","Pristina","SCRIPTINA","Segoe UI Light","Serifa","SimHei","Small Fonts","Staccato222 BT","TRAJAN PRO","Univers CE 55 Medium","Vrinda","ZWAdobeF"];function O(t){let e,n,o=!1;const[i,r]=function(){const t=document.createElement("canvas");return t.width=1,t.height=1,[t,t.getContext("2d")]}();return!function(t,e){return!(!e||!t.toDataURL)}(i,r)?e=n="unsupported":(o=function(t){return t.rect(0,0,10,10),t.rect(2,2,6,6),!t.isPointInPath(5,5,"evenodd")}(r),t?e=n="skipped":[e,n]=function(t,e){!function(t,e){t.width=240,t.height=60,e.textBaseline="alphabetic",e.fillStyle="#f60",e.fillRect(100,1,62,20),e.fillStyle="#069",e.font='11pt "Times New Roman"';const n=`Cwm fjordbank gly ${String.fromCharCode(55357,56835)}`;e.fillText(n,2,15),e.fillStyle="rgba(102, 204, 0, 0.2)",e.font="18pt Arial",e.fillText(n,4,45)}(t,e);const n=$(t),o=$(t);if(n!==o)return["unstable","unstable"];!function(t,e){t.width=122,t.height=110,e.globalCompositeOperation="multiply";for(const[n,o,i]of[["#f2f",40,40],["#2ff",80,40],["#ff2",60,80]])e.fillStyle=n,e.beginPath(),e.arc(o,i,40,0,2*Math.PI,!0),e.closePath(),e.fill();e.fillStyle="#f9c",e.arc(60,60,60,0,2*Math.PI,!0),e.arc(60,60,20,0,2*Math.PI,!0),e.fill("evenodd")}(t,e);const i=$(t);return[i,n]}(i,r)),{winding:o,geometry:e,text:n}}function $(t){return t.toDataURL()}function U(){const t=screen,e=t=>u(c(t),null),n=[e(t.width),e(t.height)];return n.sort().reverse(),n}const Q=2500;let K,q;function tt(){return function(){if(void 0!==q)return;const t=()=>{const e=et();nt(e)?q=setTimeout(t,Q):(K=e,q=void 0)};t()}(),async()=>{let t=et();if(nt(t)){if(K)return[...K];E()&&(await function(){const t=document;return(t.exitFullscreen||t.msExitFullscreen||t.mozCancelFullScreen||t.webkitExitFullscreen).call(t)}(),t=et())}return nt(t)||(K=t),t}}function et(){const t=screen;return[u(s(t.availTop),null),u(s(t.width)-s(t.availWidth)-u(s(t.availLeft),0),null),u(s(t.height)-s(t.availHeight)-u(s(t.availTop),0),null),u(s(t.availLeft),null)]}function nt(t){for(let e=0;e<4;++e)if(t[e])return!1;return!0}function ot(){return u(c(navigator.hardwareConcurrency),void 0)}function it(t){t.style.setProperty("visibility","hidden","important"),t.style.setProperty("display","block","important")}function rt(t){return matchMedia(`(inverted-colors: ${t})`).matches}function at(t){return matchMedia(`(forced-colors: ${t})`).matches}function ct(t){return matchMedia(`(prefers-contrast: ${t})`).matches}function st(t){return matchMedia(`(prefers-reduced-motion: ${t})`).matches}function ut(t){return matchMedia(`(prefers-reduced-transparency: ${t})`).matches}function lt(t){return matchMedia(`(dynamic-range: ${t})`).matches}const dt=Math,mt=()=>0;const ft="mmMwWLliI0fiflO&1",pt={default:[],apple:[{font:"-apple-system-body"}],serif:[{fontFamily:"serif"}],sans:[{fontFamily:"sans-serif"}],mono:[{fontFamily:"monospace"}],min:[{fontSize:"1px"}],system:[{fontFamily:"system-ui"}]};function ht(t){const e=H()?0:3,n=Math.pow(10,e);return Math.floor(t*n)/n}const bt=function(){let t=window;for(;;){const n=t.parent;if(!n||n===t)return!1;try{if(n.location.origin!==t.location.origin)return!0}catch(e){if(e instanceof Error&&"SecurityError"===e.name)return!0;throw e}t=n}};const yt=new Set([10752,2849,2884,2885,2886,2928,2929,2930,2931,2932,2960,2961,2962,2963,2964,2965,2966,2967,2968,2978,3024,3042,3088,3089,3106,3107,32773,32777,32777,32823,32824,32936,32937,32938,32939,32968,32969,32970,32971,3317,33170,3333,3379,3386,33901,33902,34016,34024,34076,3408,3410,3411,3412,3413,3414,3415,34467,34816,34817,34818,34819,34877,34921,34930,35660,35661,35724,35738,35739,36003,36004,36005,36347,36348,36349,37440,37441,37443,7936,7937,7938]),gt=new Set([34047,35723,36063,34852,34853,34854,34229,36392,36795,38449]),vt=["FRAGMENT_SHADER","VERTEX_SHADER"],wt=["LOW_FLOAT","MEDIUM_FLOAT","HIGH_FLOAT","LOW_INT","MEDIUM_INT","HIGH_INT"],Lt="WEBGL_debug_renderer_info";function kt(t){if(t.webgl)return t.webgl.context;const e=document.createElement("canvas");let n;e.addEventListener("webglCreateContextError",(()=>n=void 0));for(const i of["webgl","experimental-webgl"]){try{n=e.getContext(i)}catch(o){}if(n)break}return t.webgl={context:n},n}function Vt(t,e,n){const o=t.getShaderPrecisionFormat(t[e],t[n]);return o?[o.rangeMin,o.rangeMax,o.precision]:[]}function St(t){return Object.keys(t.__proto__).filter(Wt)}function Wt(t){return"string"==typeof t&&!t.match(/[^A-Z0-9_x]/)}function xt(){return Y()}function Zt(t){return"function"==typeof t.getParameter}const Mt={userAgentData:async function(){const t=navigator.userAgentData;if(!t)return;const e=t.brands.filter((({brand:t})=>!function(t){return/not/i.test(t)}(t))).map((({brand:t})=>t)),n={brands:e.length>1?e.filter((t=>"Chromium"!==t)):e,mobile:t.mobile,platform:t.platform};if(t.getHighEntropyValues)try{const e=await t.getHighEntropyValues(["architecture","bitness","model","platformVersion"]);n.architecture=e.architecture,n.bitness=e.bitness,n.model=e.model,n.platformVersion=e.platformVersion}catch(o){if(!(o instanceof DOMException&&"NotAllowedError"===o.name))throw o;n.highEntropyStatus="not_allowed"}return n},fonts:function(){return T((async(t,{document:e})=>{const n=e.body;n.style.fontSize="48px";const o=e.createElement("div");o.style.setProperty("visibility","hidden","important");const i={},r={},a=t=>{const n=e.createElement("span"),{style:i}=n;return i.position="absolute",i.top="0",i.left="0",i.fontFamily=t,n.textContent="mmMwWLliI0O&1",o.appendChild(n),n},c=(t,e)=>a(`'${t}',${e}`),s=z.map(a),u=(()=>{const t={};for(const e of B)t[e]=z.map((t=>c(e,t)));return t})();n.appendChild(o);for(let l=0;l{return e=u[t],z.some(((t,n)=>e[n].offsetWidth!==i[t]||e[n].offsetHeight!==r[t]));var e}))}))},domBlockers:async function({debug:t}={}){if(!G()&&!H())return;const e=function(){const t=atob;return{abpIndo:["#Iklan-Melayang","#Kolom-Iklan-728","#SidebarIklan-wrapper",'[title="ALIENBOLA" i]',t("I0JveC1CYW5uZXItYWRz")],abpvn:[".quangcao","#mobileCatfish",t("LmNsb3NlLWFkcw=="),'[id^="bn_bottom_fixed_"]',"#pmadv"],adBlockFinland:[".mainostila",t("LnNwb25zb3JpdA=="),".ylamainos",t("YVtocmVmKj0iL2NsaWNrdGhyZ2guYXNwPyJd"),t("YVtocmVmXj0iaHR0cHM6Ly9hcHAucmVhZHBlYWsuY29tL2FkcyJd")],adBlockPersian:["#navbar_notice_50",".kadr",'TABLE[width="140px"]',"#divAgahi",t("YVtocmVmXj0iaHR0cDovL2cxLnYuZndtcm0ubmV0L2FkLyJd")],adBlockWarningRemoval:["#adblock-honeypot",".adblocker-root",".wp_adblock_detect",t("LmhlYWRlci1ibG9ja2VkLWFk"),t("I2FkX2Jsb2NrZXI=")],adGuardAnnoyances:[".hs-sosyal","#cookieconsentdiv",'div[class^="app_gdpr"]',".as-oil",'[data-cypress="soft-push-notification-modal"]'],adGuardBase:[".BetterJsPopOverlay",t("I2FkXzMwMFgyNTA="),t("I2Jhbm5lcmZsb2F0MjI="),t("I2NhbXBhaWduLWJhbm5lcg=="),t("I0FkLUNvbnRlbnQ=")],adGuardChinese:[t("LlppX2FkX2FfSA=="),t("YVtocmVmKj0iLmh0aGJldDM0LmNvbSJd"),"#widget-quan",t("YVtocmVmKj0iLzg0OTkyMDIwLnh5eiJd"),t("YVtocmVmKj0iLjE5NTZobC5jb20vIl0=")],adGuardFrench:["#pavePub",t("LmFkLWRlc2t0b3AtcmVjdGFuZ2xl"),".mobile_adhesion",".widgetadv",t("LmFkc19iYW4=")],adGuardGerman:['aside[data-portal-id="leaderboard"]'],adGuardJapanese:["#kauli_yad_1",t("YVtocmVmXj0iaHR0cDovL2FkMi50cmFmZmljZ2F0ZS5uZXQvIl0="),t("Ll9wb3BJbl9pbmZpbml0ZV9hZA=="),t("LmFkZ29vZ2xl"),t("Ll9faXNib29zdFJldHVybkFk")],adGuardMobile:[t("YW1wLWF1dG8tYWRz"),t("LmFtcF9hZA=="),'amp-embed[type="24smi"]',"#mgid_iframe1",t("I2FkX2ludmlld19hcmVh")],adGuardRussian:[t("YVtocmVmXj0iaHR0cHM6Ly9hZC5sZXRtZWFkcy5jb20vIl0="),t("LnJlY2xhbWE="),'div[id^="smi2adblock"]',t("ZGl2W2lkXj0iQWRGb3hfYmFubmVyXyJd"),"#psyduckpockeball"],adGuardSocial:[t("YVtocmVmXj0iLy93d3cuc3R1bWJsZXVwb24uY29tL3N1Ym1pdD91cmw9Il0="),t("YVtocmVmXj0iLy90ZWxlZ3JhbS5tZS9zaGFyZS91cmw/Il0="),".etsy-tweet","#inlineShare",".popup-social"],adGuardSpanishPortuguese:["#barraPublicidade","#Publicidade","#publiEspecial","#queTooltip",".cnt-publi"],adGuardTrackingProtection:["#qoo-counter",t("YVtocmVmXj0iaHR0cDovL2NsaWNrLmhvdGxvZy5ydS8iXQ=="),t("YVtocmVmXj0iaHR0cDovL2hpdGNvdW50ZXIucnUvdG9wL3N0YXQucGhwIl0="),t("YVtocmVmXj0iaHR0cDovL3RvcC5tYWlsLnJ1L2p1bXAiXQ=="),"#top100counter"],adGuardTurkish:["#backkapat",t("I3Jla2xhbWk="),t("YVtocmVmXj0iaHR0cDovL2Fkc2Vydi5vbnRlay5jb20udHIvIl0="),t("YVtocmVmXj0iaHR0cDovL2l6bGVuemkuY29tL2NhbXBhaWduLyJd"),t("YVtocmVmXj0iaHR0cDovL3d3dy5pbnN0YWxsYWRzLm5ldC8iXQ==")],bulgarian:[t("dGQjZnJlZW5ldF90YWJsZV9hZHM="),"#ea_intext_div",".lapni-pop-over","#xenium_hot_offers"],easyList:[".yb-floorad",t("LndpZGdldF9wb19hZHNfd2lkZ2V0"),t("LnRyYWZmaWNqdW5reS1hZA=="),".textad_headline",t("LnNwb25zb3JlZC10ZXh0LWxpbmtz")],easyListChina:[t("LmFwcGd1aWRlLXdyYXBbb25jbGljayo9ImJjZWJvcy5jb20iXQ=="),t("LmZyb250cGFnZUFkdk0="),"#taotaole","#aafoot.top_box",".cfa_popup"],easyListCookie:[".ezmob-footer",".cc-CookieWarning","[data-cookie-number]",t("LmF3LWNvb2tpZS1iYW5uZXI="),".sygnal24-gdpr-modal-wrap"],easyListCzechSlovak:["#onlajny-stickers",t("I3Jla2xhbW5pLWJveA=="),t("LnJla2xhbWEtbWVnYWJvYXJk"),".sklik",t("W2lkXj0ic2tsaWtSZWtsYW1hIl0=")],easyListDutch:[t("I2FkdmVydGVudGll"),t("I3ZpcEFkbWFya3RCYW5uZXJCbG9jaw=="),".adstekst",t("YVtocmVmXj0iaHR0cHM6Ly94bHR1YmUubmwvY2xpY2svIl0="),"#semilo-lrectangle"],easyListGermany:["#SSpotIMPopSlider",t("LnNwb25zb3JsaW5rZ3J1ZW4="),t("I3dlcmJ1bmdza3k="),t("I3Jla2xhbWUtcmVjaHRzLW1pdHRl"),t("YVtocmVmXj0iaHR0cHM6Ly9iZDc0Mi5jb20vIl0=")],easyListItaly:[t("LmJveF9hZHZfYW5udW5jaQ=="),".sb-box-pubbliredazionale",t("YVtocmVmXj0iaHR0cDovL2FmZmlsaWF6aW9uaWFkcy5zbmFpLml0LyJd"),t("YVtocmVmXj0iaHR0cHM6Ly9hZHNlcnZlci5odG1sLml0LyJd"),t("YVtocmVmXj0iaHR0cHM6Ly9hZmZpbGlhemlvbmlhZHMuc25haS5pdC8iXQ==")],easyListLithuania:[t("LnJla2xhbW9zX3RhcnBhcw=="),t("LnJla2xhbW9zX251b3JvZG9z"),t("aW1nW2FsdD0iUmVrbGFtaW5pcyBza3lkZWxpcyJd"),t("aW1nW2FsdD0iRGVkaWt1b3RpLmx0IHNlcnZlcmlhaSJd"),t("aW1nW2FsdD0iSG9zdGluZ2FzIFNlcnZlcmlhaS5sdCJd")],estonian:[t("QVtocmVmKj0iaHR0cDovL3BheTRyZXN1bHRzMjQuZXUiXQ==")],fanboyAnnoyances:["#ac-lre-player",".navigate-to-top","#subscribe_popup",".newsletter_holder","#back-top"],fanboyAntiFacebook:[".util-bar-module-firefly-visible"],fanboyEnhancedTrackers:[".open.pushModal","#issuem-leaky-paywall-articles-zero-remaining-nag","#sovrn_container",'div[class$="-hide"][zoompage-fontsize][style="display: block;"]',".BlockNag__Card"],fanboySocial:["#FollowUs","#meteored_share","#social_follow",".article-sharer",".community__social-desc"],frellwitSwedish:[t("YVtocmVmKj0iY2FzaW5vcHJvLnNlIl1bdGFyZ2V0PSJfYmxhbmsiXQ=="),t("YVtocmVmKj0iZG9rdG9yLXNlLm9uZWxpbmsubWUiXQ=="),"article.category-samarbete",t("ZGl2LmhvbGlkQWRz"),"ul.adsmodern"],greekAdBlock:[t("QVtocmVmKj0iYWRtYW4ub3RlbmV0LmdyL2NsaWNrPyJd"),t("QVtocmVmKj0iaHR0cDovL2F4aWFiYW5uZXJzLmV4b2R1cy5nci8iXQ=="),t("QVtocmVmKj0iaHR0cDovL2ludGVyYWN0aXZlLmZvcnRobmV0LmdyL2NsaWNrPyJd"),"DIV.agores300","TABLE.advright"],hungarian:["#cemp_doboz",".optimonk-iframe-container",t("LmFkX19tYWlu"),t("W2NsYXNzKj0iR29vZ2xlQWRzIl0="),"#hirdetesek_box"],iDontCareAboutCookies:['.alert-info[data-block-track*="CookieNotice"]',".ModuleTemplateCookieIndicator",".o--cookies--container","#cookies-policy-sticky","#stickyCookieBar"],icelandicAbp:[t("QVtocmVmXj0iL2ZyYW1ld29yay9yZXNvdXJjZXMvZm9ybXMvYWRzLmFzcHgiXQ==")],latvian:[t("YVtocmVmPSJodHRwOi8vd3d3LnNhbGlkemluaS5sdi8iXVtzdHlsZT0iZGlzcGxheTogYmxvY2s7IHdpZHRoOiAxMjBweDsgaGVpZ2h0OiA0MHB4OyBvdmVyZmxvdzogaGlkZGVuOyBwb3NpdGlvbjogcmVsYXRpdmU7Il0="),t("YVtocmVmPSJodHRwOi8vd3d3LnNhbGlkemluaS5sdi8iXVtzdHlsZT0iZGlzcGxheTogYmxvY2s7IHdpZHRoOiA4OHB4OyBoZWlnaHQ6IDMxcHg7IG92ZXJmbG93OiBoaWRkZW47IHBvc2l0aW9uOiByZWxhdGl2ZTsiXQ==")],listKr:[t("YVtocmVmKj0iLy9hZC5wbGFuYnBsdXMuY28ua3IvIl0="),t("I2xpdmVyZUFkV3JhcHBlcg=="),t("YVtocmVmKj0iLy9hZHYuaW1hZHJlcC5jby5rci8iXQ=="),t("aW5zLmZhc3R2aWV3LWFk"),".revenue_unit_item.dable"],listeAr:[t("LmdlbWluaUxCMUFk"),".right-and-left-sponsers",t("YVtocmVmKj0iLmFmbGFtLmluZm8iXQ=="),t("YVtocmVmKj0iYm9vcmFxLm9yZyJd"),t("YVtocmVmKj0iZHViaXp6bGUuY29tL2FyLz91dG1fc291cmNlPSJd")],listeFr:[t("YVtocmVmXj0iaHR0cDovL3Byb21vLnZhZG9yLmNvbS8iXQ=="),t("I2FkY29udGFpbmVyX3JlY2hlcmNoZQ=="),t("YVtocmVmKj0id2Vib3JhbWEuZnIvZmNnaS1iaW4vIl0="),".site-pub-interstitiel",'div[id^="crt-"][data-criteo-id]'],officialPolish:["#ceneo-placeholder-ceneo-12",t("W2hyZWZePSJodHRwczovL2FmZi5zZW5kaHViLnBsLyJd"),t("YVtocmVmXj0iaHR0cDovL2Fkdm1hbmFnZXIudGVjaGZ1bi5wbC9yZWRpcmVjdC8iXQ=="),t("YVtocmVmXj0iaHR0cDovL3d3dy50cml6ZXIucGwvP3V0bV9zb3VyY2UiXQ=="),t("ZGl2I3NrYXBpZWNfYWQ=")],ro:[t("YVtocmVmXj0iLy9hZmZ0cmsuYWx0ZXgucm8vQ291bnRlci9DbGljayJd"),t("YVtocmVmXj0iaHR0cHM6Ly9ibGFja2ZyaWRheXNhbGVzLnJvL3Ryay9zaG9wLyJd"),t("YVtocmVmXj0iaHR0cHM6Ly9ldmVudC4ycGVyZm9ybWFudC5jb20vZXZlbnRzL2NsaWNrIl0="),t("YVtocmVmXj0iaHR0cHM6Ly9sLnByb2ZpdHNoYXJlLnJvLyJd"),'a[href^="/url/"]'],ruAd:[t("YVtocmVmKj0iLy9mZWJyYXJlLnJ1LyJd"),t("YVtocmVmKj0iLy91dGltZy5ydS8iXQ=="),t("YVtocmVmKj0iOi8vY2hpa2lkaWtpLnJ1Il0="),"#pgeldiz",".yandex-rtb-block"],thaiAds:["a[href*=macau-uta-popup]",t("I2Fkcy1nb29nbGUtbWlkZGxlX3JlY3RhbmdsZS1ncm91cA=="),t("LmFkczMwMHM="),".bumq",".img-kosana"],webAnnoyancesUltralist:["#mod-social-share-2","#social-tools",t("LmN0cGwtZnVsbGJhbm5lcg=="),".zergnet-recommend",".yt.btn-link.btn-md.btn"]}}(),o=Object.keys(e),i=[].concat(...o.map((t=>e[t]))),r=await async function(t){var e;const o=document,i=o.createElement("div"),r=new Array(t.length),a={};it(i);for(let n=0;n{const n=e[t];return l(n.map((t=>r[t])))>.6*n.length}));return a.sort(),a},fontPreferences:function(){return function(t,e=4e3){return T(((n,o)=>{const i=o.document,r=i.body,a=r.style;a.width=`${e}px`,a.webkitTextSizeAdjust=a.textSizeAdjust="none",F()?r.style.zoom=""+1/o.devicePixelRatio:G()&&(r.style.zoom="reset");const c=i.createElement("div");return c.textContent=[...Array(e/20|0)].map((()=>"word")).join(" "),r.appendChild(c),t(i,r,o)}),'')}(((t,e,n)=>{const o={},i={};for(const a of Object.keys(pt)){const[n={},i=ft]=pt[a],r=t.createElement("span");r.textContent=i,r.style.whiteSpace="nowrap";for(const t of Object.keys(n)){const e=n[t];void 0!==e&&(r.style[t]=e)}o[a]=r,e.append(t.createElement("br"),r)}const r=F()&&X();for(const a of Object.keys(pt)){const t=o[a].getBoundingClientRect().width;i[a]=r?ht(t*n.devicePixelRatio):t}return i}))},audio:function(){return G()&&j()&&C()||F()&&A()&&function(){const t=window,{URLPattern:e}=t;return l(["union"in Set.prototype,"Iterator"in t,e&&"hasRegExpGroups"in e.prototype,"RGB8"in WebGLRenderingContext.prototype])>=3}()?-4:N()},screenFrame:function(){const t=G()&&j()&&C(),e=Y()&&P();if(t||e)return()=>Promise.resolve(void 0);const n=tt();return async()=>{const t=await n(),e=t=>null===t?null:d(t,10);return[e(t[0]),e(t[1]),e(t[2]),e(t[3])]}},canvas:function(){return O(function(){const t=G()&&j()&&C(),e=Y()&&function(){const t=window,e=navigator,{CSS:n}=t;return l(["userActivation"in e,n.supports("color","light-dark(#000, #fff)"),n.supports("height","1lh"),"globalPrivacyControl"in e])>=3}();return t||e}())},osCpu:function(){return navigator.oscpu},languages:function(){const t=navigator,e=[],n=t.language||t.userLanguage||t.browserLanguage||t.systemLanguage;if(void 0!==n&&e.push([n]),Array.isArray(t.languages))F()&&function(){const t=window;return l([!("MediaSettingsRange"in t),"RTCEncodedAudioFrame"in t,""+t.Intl=="[object Intl]",""+t.Reflect=="[object Reflect]"])>=3}()||e.push(t.languages);else if("string"==typeof t.languages){const n=t.languages;n&&e.push(n.split(","))}return e},colorDepth:function(){return window.screen.colorDepth},deviceMemory:function(){return u(s(navigator.deviceMemory),void 0)},screenResolution:function(){if(!(G()&&j()&&C()))return U()},hardwareConcurrency:function(){const t=ot();return void 0!==t&&Y()&&P()?t>=8?8:4:t},timezone:function(){var t;const e=null===(t=window.Intl)||void 0===t?void 0:t.DateTimeFormat;if(e){const t=(new e).resolvedOptions().timeZone;if(t)return t}const n=-function(){const t=(new Date).getFullYear();return Math.max(s(new Date(t,0,1).getTimezoneOffset()),s(new Date(t,6,1).getTimezoneOffset()))}();return`UTC${n>=0?"+":""}${n}`},sessionStorage:function(){try{return!!window.sessionStorage}catch(t){return!0}},localStorage:function(){try{return!!window.localStorage}catch(t){return!0}},indexedDB:function(){if(!M()&&!R())try{return!!window.indexedDB}catch(t){return!0}},openDatabase:function(){return!!window.openDatabase},cpuClass:function(){return navigator.cpuClass},platform:function(){const{platform:t}=navigator;return"MacIntel"===t&&G()&&!I()?function(){if("iPad"===navigator.platform)return!0;const t=screen,e=t.width/t.height;return l(["MediaSource"in window,!!Element.prototype.webkitRequestFullscreen,e>.65&&e<1.53])>=2}()?"iPad":"iPhone":t},plugins:function(){const t=navigator.plugins;if(!t)return;const e=[];for(let n=0;ndt.log(t+dt.sqrt(t*t+1)))(1),atanh:i(.5),atanhPf:(t=>dt.log((1+t)/(1-t))/2)(.5),atan:r(.5),sin:a(-1e300),sinh:c(1),sinhPf:(t=>dt.exp(t)-1/dt.exp(t)/2)(1),cos:s(10.000000000123),cosh:u(1),coshPf:(t=>(dt.exp(t)+1/dt.exp(t))/2)(1),tan:l(-1e300),tanh:d(1),tanhPf:(t=>(dt.exp(2*t)-1)/(dt.exp(2*t)+1))(1),exp:m(1),expm1:f(1),expm1Pf:(t=>dt.exp(t)-1)(1),log1p:p(10),log1pPf:(t=>dt.log(1+t))(10),powPI:(t=>dt.pow(dt.PI,t))(-100)};var h},pdfViewerEnabled:function(){return navigator.pdfViewerEnabled},architecture:function(){const t=new Float32Array(1),e=new Uint8Array(t.buffer);return t[0]=1/0,t[0]=t[0]-t[0],e[3]},applePay:function(){const{ApplePaySession:t}=window;if("function"!=typeof(null==t?void 0:t.canMakePayments))return-1;if(bt())return-3;try{return t.canMakePayments()?1:0}catch(e){return function(t){if(t instanceof Error&&"InvalidAccessError"===t.name&&/\bfrom\b.*\binsecure\b/i.test(t.message))return-2;throw t}(e)}},privateClickMeasurement:function(){var t;const e=document.createElement("a"),n=null!==(t=e.attributionSourceId)&&void 0!==t?t:e.attributionsourceid;return void 0===n?void 0:String(n)},audioBaseLatency:function(){if(!(H()||G()))return-2;if(!window.AudioContext)return-1;const t=(new AudioContext).baseLatency;return null==t?-1:isFinite(t)?t:-3},dateTimeLocale:function(){if(!window.Intl)return-1;const t=window.Intl.DateTimeFormat;if(!t)return-2;const e=t().resolvedOptions().locale;return e||""===e?e:-3},webGlBasics:function({cache:t}){var e,n,o,i,r,a;const c=kt(t);if(!c)return-1;if(!Zt(c))return-2;const s=xt()?null:c.getExtension(Lt);return{version:(null===(e=c.getParameter(c.VERSION))||void 0===e?void 0:e.toString())||"",vendor:(null===(n=c.getParameter(c.VENDOR))||void 0===n?void 0:n.toString())||"",vendorUnmasked:s?null===(o=c.getParameter(s.UNMASKED_VENDOR_WEBGL))||void 0===o?void 0:o.toString():"",renderer:(null===(i=c.getParameter(c.RENDERER))||void 0===i?void 0:i.toString())||"",rendererUnmasked:s?null===(r=c.getParameter(s.UNMASKED_RENDERER_WEBGL))||void 0===r?void 0:r.toString():"",shadingLanguageVersion:(null===(a=c.getParameter(c.SHADING_LANGUAGE_VERSION))||void 0===a?void 0:a.toString())||""}},webGlExtensions:function({cache:t}){const e=kt(t);if(!e)return-1;if(!Zt(e))return-2;const n=e.getSupportedExtensions(),o=e.getContextAttributes(),i=[],r=[],a=[],c=[],s=[];if(o)for(const l of Object.keys(o))r.push(`${l}=${o[l]}`);const u=St(e);for(const l of u){const t=e[l];a.push(`${l}=${t}${yt.has(t)?`=${e.getParameter(t)}`:""}`)}if(n)for(const l of n){if(l===Lt&&xt()||"WEBGL_polygon_mode"===l&&(F()||G()))continue;const t=e.getExtension(l);if(t)for(const n of St(t)){const o=t[n];c.push(`${n}=${o}${gt.has(o)?`=${e.getParameter(o)}`:""}`)}else i.push(l)}for(const l of vt)for(const t of wt){const n=Vt(e,l,t);s.push(`${l}.${t}=${n.join(",")}`)}return c.sort(),a.sort(),{contextAttributes:r,parameters:a,shaderPrecisions:s,extensions:n,extensionParameters:c,unsupportedExtensions:i}}};const Rt="$ if upgrade to Pro: https://fingerprint.com/github/?utm_source=oss&utm_medium=referral&utm_campaign=confidence_score";function Ft(t){const e=function(t){if(H())return.4;if(G())return!I()||j()&&C()?.3:.5;const e="value"in t.platform?t.platform.value:"";if(/^Win/.test(e))return.6;if(/^Mac/.test(e))return.5;return.7}(t),n=function(t){return d(.99+.01*t,1e-4)}(e);return{score:e,comment:Rt.replace(/\$/g,`${n}`)}}function Gt(t){return JSON.stringify(t,((t,e)=>{return e instanceof Error?{name:(n=e).name,message:n.message,stack:null===(o=n.stack)||void 0===o?void 0:o.split("\n"),...n}:e;var n,o}),2)}function It(t){return W(function(t){let e="";for(const n of Object.keys(t).sort()){const o=t[n],i="error"in o?"error":JSON.stringify(o.value);e+=`${e?"|":""}${n.replace(/([:|\\])/g,"\\$1")}:${i}`}return e}(t))}function Ct(t=50){return function(t,e=1/0){const{requestIdleCallback:o}=window;return o?new Promise((t=>o.call(window,(()=>t()),{timeout:e}))):n(Math.min(t,e))}(t,2*t)}function Yt(t,n){const o=Date.now();return{async get(i){const r=Date.now(),a=await t(),c=function(t){let n;const o=Ft(t);return{get visitorId(){return void 0===n&&(n=It(this.components)),n},set visitorId(t){n=t},confidence:o,components:t,version:e}}(a);return(n||(null==i?void 0:i.debug))&&console.log(`Copy the text below to get the debug data:\n\n\`\`\`\nversion: ${c.version}\nuserAgent: ${navigator.userAgent}\ntimeBetweenLoadAndGet: ${r-o}\nvisitorId: ${c.visitorId}\ncomponents: ${Gt(a)}\n\`\`\``),c}}}async function Pt(t={}){const{delayFallback:n,debug:o,monitoring:i=!0}=t;i&&function(){if(!(window.__fpjs_d_m||Math.random()>=.001))try{const t=new XMLHttpRequest;t.open("get",`https://m1.openfpcdn.io/fingerprintjs/v${e}/npm-monitoring`,!0),t.send()}catch(t){console.error(t)}}(),await Ct(n);const r=function(t){return Z(Mt,t,[])}({cache:{},debug:o});return Yt(r,o)}var Xt={load:Pt,hashComponents:It,componentsToDebugString:Gt};const jt=W;t.componentsToDebugString=Gt,t.default=Xt,t.getFullscreenElement=E,t.getUnstableAudioFingerprint=N,t.getUnstableCanvasFingerprint=O,t.getUnstableHardwareConcurrency=ot,t.getUnstableScreenFrame=tt,t.getUnstableScreenResolution=U,t.getWebGLContext=kt,t.hashComponents=It,t.isAndroid=H,t.isChromium=F,t.isDesktopWebKit=I,t.isEdgeHTML=R,t.isGecko=Y,t.isSamsungInternet=A,t.isTrident=M,t.isWebKit=G,t.load=Pt,t.loadSources=Z,t.murmurX64Hash128=jt,t.prepareForSources=Ct,t.sources=Mt,t.transformSource=function(t,e){const n=t=>x(t)?e(t):()=>{const n=t();return o(n)?n.then(e):e(n)};return e=>{const i=t(e);return o(i)?i.then(n):n(i)}},t.withIframe=T,Object.defineProperty(t,"__esModule",{value:!0})})); diff --git a/tests/vendor/fpscanner-1.0.6.es.js b/tests/vendor/fpscanner-1.0.6.es.js deleted file mode 100644 index 4df30e4..0000000 --- a/tests/vendor/fpscanner-1.0.6.es.js +++ /dev/null @@ -1,1253 +0,0 @@ -function ie() { - return navigator.webdriver; -} -function ae() { - return navigator.userAgent; -} -function oe() { - return navigator.platform; -} -const l = "ERROR", r = "INIT", s = "NA", v = "SKIPPED", h = "high", S = "low", se = "medium"; -function f(t) { - let e = 0; - for (let n = 0, i = t.length; n < i; n++) { - let a = t.charCodeAt(n); - e = (e << 5) - e + a, e |= 0; - } - return e.toString(16).padStart(8, "0"); -} -function d(t, e) { - for (const n in t) - t[n] = e; -} -function ce() { - return navigator.buildID === "20181001000000"; -} -function le() { - try { - let t = !1; - const e = Error.prepareStackTrace; - Error.prepareStackTrace = function() { - return t = !0, e; - }; - const n = new Error(""); - return console.log(n), t; - } catch { - return l; - } -} -function ue() { - const t = { - vendor: r, - renderer: r - }; - if (ce()) - return d(t, s), t; - try { - var e = document.createElement("canvas"), n = e.getContext("webgl") || e.getContext("experimental-webgl"); - n.getSupportedExtensions().indexOf("WEBGL_debug_renderer_info") >= 0 ? (t.vendor = n.getParameter(n.getExtension("WEBGL_debug_renderer_info").UNMASKED_VENDOR_WEBGL), t.renderer = n.getParameter(n.getExtension("WEBGL_debug_renderer_info").UNMASKED_RENDERER_WEBGL)) : d(t, s); - } catch { - d(t, l); - } - return t; -} -function de() { - return "__pwInitScripts" in window || "__playwright__binding__" in window; -} -function ge() { - return navigator.hardwareConcurrency || s; -} -function he() { - const t = [], e = 0.123456789; - return ["E", "LN10", "LN2", "LOG10E", "LOG2E", "PI", "SQRT1_2", "SQRT2"].forEach(function(a) { - try { - t.push(Math[a]); - } catch { - t.push(-1); - } - }), ["tan", "sin", "exp", "atan", "acosh", "asinh", "atanh", "expm1", "log1p", "sinh"].forEach(function(a) { - try { - t.push(Math[a](e)); - } catch { - t.push(-1); - } - }), "sumPrecise" in Math ? t.push(Math.sumPrecise([1e20, 0.1, -1e20])) : t.push(-1), f(t.map(String).join(",")); -} -function me() { - return navigator.deviceMemory || s; -} -function pe() { - return eval.toString().length; -} -function fe() { - const t = { - timezone: r, - localeLanguage: r - }; - try { - if (typeof Intl < "u" && typeof Intl.DateTimeFormat < "u") { - const e = Intl.DateTimeFormat().resolvedOptions(); - t.timezone = e.timeZone, t.localeLanguage = e.locale; - } else - t.timezone = s, t.localeLanguage = s; - } catch { - t.timezone = l, t.localeLanguage = l; - } - return t; -} -function ve() { - return { - width: window.screen.width, - height: window.screen.height, - pixelDepth: window.screen.pixelDepth, - colorDepth: window.screen.colorDepth, - availableWidth: window.screen.availWidth, - availableHeight: window.screen.availHeight, - innerWidth: window.innerWidth, - innerHeight: window.innerHeight, - hasMultipleDisplays: typeof screen.isExtended < "u" ? screen.isExtended : s - }; -} -function ye() { - return { - languages: navigator.languages, - language: navigator.language - }; -} -async function we() { - const t = { - vendor: r, - architecture: r, - device: r, - description: r - }; - if ("gpu" in navigator) - try { - const e = await navigator.gpu.requestAdapter(); - e && (t.vendor = e.info.vendor, t.architecture = e.info.architecture, t.device = e.info.device, t.description = e.info.description); - } catch { - d(t, l); - } - else - d(t, s); - return t; -} -function be() { - const t = [ - "__driver_evaluate", - "__webdriver_evaluate", - "__selenium_evaluate", - "__fxdriver_evaluate", - "__driver_unwrapped", - "__webdriver_unwrapped", - "__selenium_unwrapped", - "__fxdriver_unwrapped", - "_Selenium_IDE_Recorder", - "_selenium", - "calledSelenium", - "$cdc_asdjflasutopfhvcZLmcfl_", - "$chrome_asyncScriptInfo", - "__$webdriverAsyncExecutor", - "webdriver", - "__webdriverFunc", - "domAutomation", - "domAutomationController", - "__lastWatirAlert", - "__lastWatirConfirm", - "__lastWatirPrompt", - "__webdriver_script_fn", - "_WEBDRIVER_ELEM_CACHE" - ]; - let e = !1; - for (let n = 0; n < t.length; n++) - if (t[n] in window) { - e = !0; - break; - } - return e = e || !!document.__webdriver_script_fn || !!window.domAutomation || !!window.domAutomationController, e; -} -function Se() { - try { - const t = "webdriver", e = window.navigator; - if (!e[t] && !e.hasOwnProperty(t)) { - e[t] = 1; - const n = e[t] === 1; - return delete e[t], n; - } - return !0; - } catch { - return !1; - } -} -async function Ce() { - const t = window.navigator, e = { - architecture: r, - bitness: r, - brands: r, - mobile: r, - model: r, - platform: r, - platformVersion: r, - uaFullVersion: r - }; - if ("userAgentData" in t) - try { - const n = await t.userAgentData.getHighEntropyValues([ - "architecture", - "bitness", - "brands", - "mobile", - "model", - "platform", - "platformVersion", - "uaFullVersion" - ]); - e.architecture = n.architecture, e.bitness = n.bitness, e.brands = n.brands, e.mobile = n.mobile, e.model = n.model, e.platform = n.platform, e.platformVersion = n.platformVersion, e.uaFullVersion = n.uaFullVersion; - } catch { - d(e, l); - } - else - d(e, s); - return e; -} -function Ae() { - if (!navigator.plugins) return !1; - const t = typeof navigator.plugins.toString == "function" ? navigator.plugins.toString() : navigator.plugins.constructor && typeof navigator.plugins.constructor.toString == "function" ? navigator.plugins.constructor.toString() : typeof navigator.plugins; - return t === "[object PluginArray]" || t === "[object MSPluginsCollection]" || t === "[object HTMLPluginsCollection]"; -} -function Pe() { - if (!navigator.plugins) return s; - const t = []; - for (let e = 0; e < navigator.plugins.length; e++) - t.push(navigator.plugins[e].name); - return f(t.join(",")); -} -function ke() { - return navigator.plugins ? navigator.plugins.length : s; -} -function Me() { - if (!navigator.plugins) return s; - try { - return navigator.plugins[0] === navigator.plugins[0][0].enabledPlugin; - } catch { - return l; - } -} -function xe() { - if (!navigator.plugins) return s; - try { - return navigator.plugins.item(4294967296) !== navigator.plugins[0]; - } catch { - return l; - } -} -function We() { - const t = { - isValidPluginArray: r, - pluginCount: r, - pluginNamesHash: r, - pluginConsistency1: r, - pluginOverflow: r - }; - try { - t.isValidPluginArray = Ae(), t.pluginCount = ke(), t.pluginNamesHash = Pe(), t.pluginConsistency1 = Me(), t.pluginOverflow = xe(); - } catch { - d(t, l); - } - return t; -} -async function Ee() { - return new Promise(async function(t) { - var e = { - audiooutput: 0, - audioinput: 0, - videoinput: 0 - }; - if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) { - const a = await navigator.mediaDevices.enumerateDevices(); - if (typeof a < "u") { - for (var n = 0; n < a.length; n++) { - var i = a[n].kind; - e[i] = e[i] + 1; - } - return t({ - speakers: e.audiooutput, - microphones: e.audioinput, - webcams: e.videoinput - }); - } else - return d(e, s), t(e); - } else - return d(e, s), t(e); - }); -} -function De() { - const t = { - webdriver: r, - userAgent: r, - platform: r, - memory: r, - cpuCount: r, - language: r - }, e = document.createElement("iframe"); - let n = !1; - try { - e.style.display = "none", e.src = "about:blank", document.body.appendChild(e), n = !0; - const i = e.contentWindow?.navigator; - t.webdriver = i.webdriver ?? !1, t.userAgent = i.userAgent ?? s, t.platform = i.platform ?? s, t.memory = i.deviceMemory ?? s, t.cpuCount = i.hardwareConcurrency ?? s, t.language = i.language ?? s; - } catch { - d(t, l); - } finally { - if (n) - try { - document.body.removeChild(e); - } catch { - } - } - return t; -} -async function _e() { - return new Promise((t) => { - const e = { - vendor: r, - renderer: r, - userAgent: r, - language: r, - platform: r, - memory: r, - cpuCount: r - }; - let n = null, i = null, a = null; - const g = () => { - a && clearTimeout(a), n && n.terminate(), i && URL.revokeObjectURL(i); - }; - try { - const p = `var fingerprintWorker = { - userAgent: 'NA', - language: 'NA', - cpuCount: 'NA', - platform: 'NA', - memory: 'NA', - vendor: 'NA', - renderer: 'NA' - }; - try { - fingerprintWorker.userAgent = navigator.userAgent; - fingerprintWorker.language = navigator.language; - fingerprintWorker.cpuCount = navigator.hardwareConcurrency; - fingerprintWorker.platform = navigator.platform; - if (typeof navigator.deviceMemory !== 'undefined') { - fingerprintWorker.memory = navigator.deviceMemory; - } - - try { - if (typeof OffscreenCanvas === 'undefined') { - fingerprintWorker.vendor = 'NA'; - fingerprintWorker.renderer = 'NA'; - } else { - var canvas = new OffscreenCanvas(1, 1); - var gl = canvas.getContext('webgl'); - var isFirefox = navigator.userAgent.indexOf('Firefox') !== -1; - if (gl && !isFirefox) { - var glExt = gl.getExtension('WEBGL_debug_renderer_info'); - if (glExt) { - fingerprintWorker.vendor = gl.getParameter(glExt.UNMASKED_VENDOR_WEBGL); - fingerprintWorker.renderer = gl.getParameter(glExt.UNMASKED_RENDERER_WEBGL); - } else { - fingerprintWorker.vendor = 'NA'; - fingerprintWorker.renderer = 'NA'; - } - } else { - fingerprintWorker.vendor = 'NA'; - fingerprintWorker.renderer = 'NA'; - } - } - } catch (_) { - fingerprintWorker.vendor = 'ERROR'; - fingerprintWorker.renderer = 'ERROR'; - } - self.postMessage(fingerprintWorker); - } catch (e) { - self.postMessage(fingerprintWorker); - }`, y = new Blob([p], { type: "application/javascript" }); - i = URL.createObjectURL(y), n = new Worker(i), a = window.setTimeout(() => { - g(), d(e, l), t(e); - }, 2e3), n.onmessage = function(o) { - try { - const m = (w) => typeof w > "u" ? s : w; - e.vendor = m(o.data.vendor), e.renderer = m(o.data.renderer), e.userAgent = m(o.data.userAgent), e.language = m(o.data.language), e.platform = m(o.data.platform), e.memory = m(o.data.memory), e.cpuCount = m(o.data.cpuCount); - } catch { - d(e, l); - } finally { - g(), t(e); - } - }, n.onerror = function() { - g(), d(e, l), t(e); - }; - } catch { - g(), d(e, l), t(e); - } - }); -} -function Re() { - const t = { - toSourceError: r, - hasToSource: !1 - }; - try { - null.usdfsh; - } catch (e) { - t.toSourceError = e.toString(); - } - try { - throw "xyz"; - } catch (e) { - try { - e.toSource(), t.hasToSource = !0; - } catch { - t.hasToSource = !1; - } - } - return t; -} -const C = [ - 'audio/mp4; codecs="mp4a.40.2"', - "audio/mpeg;", - 'audio/webm; codecs="vorbis"', - 'audio/ogg; codecs="vorbis"', - 'audio/wav; codecs="1"', - 'audio/ogg; codecs="speex"', - 'audio/ogg; codecs="flac"', - 'audio/3gpp; codecs="samr"' -], A = [ - 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"', - 'video/mp4; codecs="avc1.42E01E"', - 'video/mp4; codecs="avc1.58A01E"', - 'video/mp4; codecs="avc1.4D401E"', - 'video/mp4; codecs="avc1.64001E"', - 'video/mp4; codecs="mp4v.20.8"', - 'video/mp4; codecs="mp4v.20.240"', - 'video/webm; codecs="vp8"', - 'video/ogg; codecs="theora"', - 'video/ogg; codecs="dirac"', - 'video/3gpp; codecs="mp4v.20.8"', - 'video/x-matroska; codecs="theora"' -]; -function P(t, e) { - const n = {}; - try { - const i = document.createElement(e); - for (const a of t) - try { - n[a] = i.canPlayType(a) || null; - } catch { - n[a] = null; - } - } catch { - for (const i of t) - n[i] = null; - } - return n; -} -function k(t) { - const e = {}, n = window.MediaSource; - if (!n || typeof n.isTypeSupported != "function") { - for (const i of t) - e[i] = null; - return e; - } - for (const i of t) - try { - e[i] = n.isTypeSupported(i); - } catch { - e[i] = null; - } - return e; -} -function M(t) { - try { - const e = window.RTCRtpReceiver; - if (e && typeof e.getCapabilities == "function") { - const n = e.getCapabilities(t); - return f(JSON.stringify(n)); - } - return s; - } catch { - return l; - } -} -function Ie() { - const t = { - audioCanPlayTypeHash: s, - videoCanPlayTypeHash: s, - audioMediaSourceHash: s, - videoMediaSourceHash: s, - rtcAudioCapabilitiesHash: s, - rtcVideoCapabilitiesHash: s, - hasMediaSource: !1 - }; - try { - t.hasMediaSource = !!window.MediaSource; - const e = P(C, "audio"), n = P(A, "video"); - t.audioCanPlayTypeHash = f(JSON.stringify(e)), t.videoCanPlayTypeHash = f(JSON.stringify(n)); - const i = k(C), a = k(A); - t.audioMediaSourceHash = f(JSON.stringify(i)), t.videoMediaSourceHash = f(JSON.stringify(a)), t.rtcAudioCapabilitiesHash = M("audio"), t.rtcVideoCapabilitiesHash = M("video"); - } catch { - d(t, l); - } - return t; -} -async function Le() { - return new Promise((t) => { - try { - const e = new Image(), n = document.createElement("canvas").getContext("2d"); - e.onload = () => { - n.drawImage(e, 0, 0), t(n.getImageData(0, 0, 1, 1).data.filter((i) => i === 0).length != 4); - }, e.onerror = () => { - t(l); - }, e.src = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQYV2NgAAIAAAUAAarVyFEAAAAASUVORK5CYII="; - } catch { - t(l); - } - }); -} -function Te() { - var t = document.createElement("canvas"); - t.width = 400, t.height = 200, t.style.display = "inline"; - var e = t.getContext("2d"); - try { - return e.rect(0, 0, 10, 10), e.rect(2, 2, 6, 6), e.textBaseline = "alphabetic", e.fillStyle = "#f60", e.fillRect(125, 1, 62, 20), e.fillStyle = "#069", e.font = "11pt no-real-font-123", e.fillText("Cwm fjordbank glyphs vext quiz, 😃", 2, 15), e.fillStyle = "rgba(102, 204, 0, 0.2)", e.font = "18pt Arial", e.fillText("Cwm fjordbank glyphs vext quiz, 😃", 4, 45), e.globalCompositeOperation = "multiply", e.fillStyle = "rgb(255,0,255)", e.beginPath(), e.arc(50, 50, 50, 0, 2 * Math.PI, !0), e.closePath(), e.fill(), e.fillStyle = "rgb(0,255,255)", e.beginPath(), e.arc(100, 50, 50, 0, 2 * Math.PI, !0), e.closePath(), e.fill(), e.fillStyle = "rgb(255,255,0)", e.beginPath(), e.arc(75, 100, 50, 0, 2 * Math.PI, !0), e.closePath(), e.fill(), e.fillStyle = "rgb(255,0,255)", e.arc(75, 75, 75, 0, 2 * Math.PI, !0), e.arc(75, 75, 25, 0, 2 * Math.PI, !0), e.fill("evenodd"), f(t.toDataURL()); - } catch { - return l; - } -} -async function Oe() { - const t = { - hasModifiedCanvas: r, - canvasFingerprint: r - }; - return t.hasModifiedCanvas = await Le(), t.canvasFingerprint = Te(), t; -} -function He() { - const t = ["deviceMemory", "hardwareConcurrency", "language", "languages", "platform"], e = []; - for (const n of t) { - const i = Object.getOwnPropertyDescriptor(Object.getPrototypeOf(navigator), n); - i && i.value ? e.push("1") : e.push("0"); - } - return e.join(""); -} -function ze() { - return Math.random().toString(36).substring(2, 15); -} -function Ue() { - return (/* @__PURE__ */ new Date()).getTime(); -} -function Ne() { - return window.location.href; -} -function x(t, e) { - const n = t.signals; - return e === "iframe" ? n.contexts.iframe.webdriver !== n.automation.webdriver || n.contexts.iframe.userAgent !== n.browser.userAgent || n.contexts.iframe.platform !== n.device.platform || n.contexts.iframe.memory !== n.device.memory || n.contexts.iframe.cpuCount !== n.device.cpuCount : n.contexts.webWorker.webdriver !== n.automation.webdriver || n.contexts.webWorker.userAgent !== n.browser.userAgent || n.contexts.webWorker.platform !== n.device.platform || n.contexts.webWorker.memory !== n.device.memory || n.contexts.webWorker.cpuCount !== n.device.cpuCount; -} -function Fe() { - const t = { - bitmask: r, - extensions: [] - }, e = document.body.hasAttribute("data-gr-ext-installed"), n = typeof window.ethereum < "u", i = document.getElementById("coupon-birds-drop-div") !== null, a = document.querySelector("deepl-input-controller") !== null, g = document.getElementById("monica-content-root") !== null, p = document.querySelector("chatgpt-sidebar") !== null, y = typeof window.__REQUESTLY__ < "u", o = Array.from(document.querySelectorAll("*")).filter((m) => m.tagName.toLowerCase().startsWith("veepn-")).length > 0; - return t.bitmask = [ - e ? "1" : "0", - n ? "1" : "0", - i ? "1" : "0", - a ? "1" : "0", - g ? "1" : "0", - p ? "1" : "0", - y ? "1" : "0", - o ? "1" : "0" - ].join(""), e && t.extensions.push("grammarly"), n && t.extensions.push("metamask"), i && t.extensions.push("coupon-birds"), a && t.extensions.push("deepl"), g && t.extensions.push("monica-ai"), p && t.extensions.push("sider-ai"), y && t.extensions.push("requestly"), o && t.extensions.push("veepn"), t; -} -function c(t) { - try { - return t(); - } catch { - return !1; - } -} -function Ge() { - const t = { - bitmask: r, - chrome: c(() => "chrome" in window), - brave: c(() => "brave" in navigator), - applePaySupport: c(() => "ApplePaySetup" in window), - opera: c(() => typeof window.opr < "u" || typeof window.onoperadetachedviewchange == "object"), - serial: c(() => window.navigator.serial !== void 0), - attachShadow: c(() => !!Element.prototype.attachShadow), - caches: c(() => !!window.caches), - webAssembly: c(() => !!window.WebAssembly && !!window.WebAssembly.instantiate), - buffer: c(() => "Buffer" in window), - showModalDialog: c(() => "showModalDialog" in window), - safari: c(() => "safari" in window), - webkitPrefixedFunction: c(() => "webkitCancelAnimationFrame" in window), - mozPrefixedFunction: c(() => "mozGetUserMedia" in navigator), - usb: c(() => typeof window.USB == "function"), - browserCapture: c(() => typeof window.BrowserCaptureMediaStreamTrack == "function"), - paymentRequestUpdateEvent: c(() => typeof window.PaymentRequestUpdateEvent == "function"), - pressureObserver: c(() => typeof window.PressureObserver == "function"), - audioSession: c(() => "audioSession" in navigator), - selectAudioOutput: c(() => typeof navigator < "u" && typeof navigator.mediaDevices < "u" && typeof navigator.mediaDevices.selectAudioOutput == "function"), - barcodeDetector: c(() => "BarcodeDetector" in window), - battery: c(() => "getBattery" in navigator), - devicePosture: c(() => "DevicePosture" in window), - documentPictureInPicture: c(() => "documentPictureInPicture" in window), - eyeDropper: c(() => "EyeDropper" in window), - editContext: c(() => "EditContext" in window), - fencedFrame: c(() => "FencedFrameConfig" in window), - sanitizer: c(() => "Sanitizer" in window), - otpCredential: c(() => "OTPCredential" in window), - sumPrecise: c(() => "sumPrecise" in Math) - }, e = Object.keys(t).filter((n) => n !== "bitmask").map((n) => t[n] ? "1" : "0").join(""); - return t.bitmask = e, t; -} -function Ve() { - const t = { - prefersColorScheme: r, - prefersReducedMotion: r, - prefersReducedTransparency: r, - colorGamut: r, - pointer: r, - anyPointer: r, - hover: r, - anyHover: r, - colorDepth: r - }; - try { - window.matchMedia("(prefers-color-scheme: dark)").matches ? t.prefersColorScheme = "dark" : window.matchMedia("(prefers-color-scheme: light)").matches ? t.prefersColorScheme = "light" : t.prefersColorScheme = null, t.prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches, t.prefersReducedTransparency = window.matchMedia("(prefers-reduced-transparency: reduce)").matches, window.matchMedia("(color-gamut: rec2020)").matches ? t.colorGamut = "rec2020" : window.matchMedia("(color-gamut: p3)").matches ? t.colorGamut = "p3" : window.matchMedia("(color-gamut: srgb)").matches ? t.colorGamut = "srgb" : t.colorGamut = null, window.matchMedia("(pointer: fine)").matches ? t.pointer = "fine" : window.matchMedia("(pointer: coarse)").matches ? t.pointer = "coarse" : window.matchMedia("(pointer: none)").matches ? t.pointer = "none" : t.pointer = null, window.matchMedia("(any-pointer: fine)").matches ? t.anyPointer = "fine" : window.matchMedia("(any-pointer: coarse)").matches ? t.anyPointer = "coarse" : window.matchMedia("(any-pointer: none)").matches ? t.anyPointer = "none" : t.anyPointer = null, t.hover = window.matchMedia("(hover: hover)").matches, t.anyHover = window.matchMedia("(any-hover: hover)").matches; - let e = 0; - for (let n = 0; n <= 16; n++) - window.matchMedia(`(color: ${n})`).matches && (e = n); - t.colorDepth = e; - } catch { - d(t, l); - } - return t; -} -async function Be() { - const t = { - layout: r, - layoutSize: r - }; - if ("keyboard" in navigator && typeof navigator.keyboard.getLayoutMap < "u") - try { - const e = await navigator.keyboard.getLayoutMap(); - t.layout = Array.from( - e.entries() - ).map(([n, i]) => `${n},${i}`).join(" "), t.layoutSize = e.size; - } catch { - d(t, l); - } - else - d(t, s); - return t; -} -async function je() { - const t = { - summarizerAvailability: r, - summarizerLanguageAvailability: r - }; - if ("Summarizer" in window) - try { - t.summarizerAvailability = await window.Summarizer.availability(), t.summarizerLanguageAvailability = await window.Summarizer.availability({ - expectedInputLanguages: [navigator.language] - }); - } catch { - d(t, l); - } - else - d(t, s); - return t; -} -function $e(t) { - const e = t.signals.device.screenResolution; - return e.width === 800 && e.height === 600 || e.availableWidth === 800 && e.availableHeight === 600 || e.innerWidth === 800 && e.innerHeight === 600; -} -function Qe(t) { - return t.signals.automation.webdriver === !0; -} -function qe(t) { - return !!t.signals.automation.selenium; -} -function Ke(t) { - return t.signals.automation.cdp === !0; -} -function Je(t) { - return t.signals.automation.playwright === !0; -} -function Ye(t) { - return typeof t.signals.device.memory != "number" ? !1 : t.signals.device.memory > 32 || t.signals.device.memory < 0.25; -} -function Ze(t) { - return typeof t.signals.device.cpuCount != "number" ? !1 : t.signals.device.cpuCount > 70; -} -function Xe(t) { - return t.includes("Android") || t.includes("iPhone") || t.includes("iPod") || t.includes("iPad"); -} -function et(t) { - const e = t.signals.browser.userAgent; - return typeof e != "string" || !e.includes("Chrome") || Xe(e) ? !1 : t.signals.browser.features.chrome === !1; -} -function tt(t) { - return t.signals.contexts.iframe.webdriver === !0; -} -function rt(t) { - return t.signals.contexts.webWorker.webdriver === !0; -} -function b(t) { - return typeof t != "string" || t.length === 0 ? !0 : t === s || t === l || t === v || t === r; -} -function nt(t) { - const e = t.signals.contexts.webWorker, n = t.signals.graphics.webGL; - return b(n.vendor) || b(n.renderer) || b(e.vendor) || b(e.renderer) ? !1 : e.vendor !== n.vendor || e.renderer !== n.renderer; -} -function it(t, e) { - const n = t.includes("iPad"), i = e.includes("iPad"); - if (n === i) - return !1; - const a = (g) => g === "MacIntel" || g === "MacPPC"; - return a(t) || a(e); -} -function at(t) { - if (t.signals.contexts.webWorker.platform === s || t.signals.contexts.webWorker.platform === l || t.signals.contexts.webWorker.platform === v) - return !1; - const e = t.signals.device.platform, n = t.signals.contexts.webWorker.platform; - return !(e === n || it(e, n)); -} -function ot(t) { - return t.signals.contexts.iframe.platform === s || t.signals.contexts.iframe.platform === l ? !1 : t.signals.device.platform !== t.signals.contexts.iframe.platform; -} -function st(t) { - return t.signals.automation.webdriverWritable === !0; -} -function ct(t) { - return t.signals.graphics.webGL.renderer.includes("SwiftShader"); -} -function lt(t) { - return t.signals.locale.internationalization.timezone === "UTC"; -} -function ut(t) { - const e = t.signals.locale.languages.languages, n = t.signals.locale.languages.language; - return n && e && Array.isArray(e) && e.length > 0 ? e[0] !== n : !1; -} -function dt(t) { - return !!(t.signals.browser.features.chrome && t.signals.browser.etsl !== 33 || t.signals.browser.features.safari && t.signals.browser.etsl !== 37 || t.signals.browser.userAgent.includes("Firefox") && t.signals.browser.etsl !== 37); -} -function gt(t) { - return [ - t.signals.browser.userAgent, - t.signals.contexts.iframe.userAgent, - t.signals.contexts.webWorker.userAgent - ].some((n) => /bot|headless/i.test(n.toLowerCase())); -} -function ht(t) { - const e = t.signals.graphics.webgpu, n = t.signals.graphics.webGL, i = t.signals.browser.userAgent; - return !!((n.vendor.includes("Apple") || n.renderer.includes("Apple")) && !i.includes("Mac") || e.vendor.includes("apple") && !i.includes("Mac") || e.vendor.includes("apple") && !n.renderer.includes("Apple")); -} -function mt(t) { - const e = t.signals.device.platform, n = t.signals.browser.userAgent, i = t.signals.browser.highEntropyValues.platform; - return !!(n.includes("Mac") && (e.includes("Win") || e.includes("Linux")) || n.includes("Windows") && (e.includes("Mac") || e.includes("Linux")) || n.includes("Linux") && (e.includes("Mac") || e.includes("Win")) || i !== l && i !== s && (i.includes("Mac") && (e.includes("Win") || e.includes("Linux")) || i.includes("Windows") && (e.includes("Mac") || e.includes("Linux")) || i.includes("Linux") && (e.includes("Mac") || e.includes("Win")))); -} -async function pt(t, e) { - const n = new TextEncoder().encode(e), i = new TextEncoder().encode(t), a = new Uint8Array(i.length); - for (let p = 0; p < i.length; p++) - a[p] = i[p] ^ n[p % n.length]; - const g = String.fromCharCode(...a); - return btoa(g); -} -class vt { - constructor() { - this.fingerprint = { - signals: { - // Automation/Bot detection signals - automation: { - webdriver: r, - webdriverWritable: r, - selenium: r, - cdp: r, - playwright: r, - navigatorPropertyDescriptors: r - }, - // Device hardware characteristics - device: { - cpuCount: r, - memory: r, - platform: r, - screenResolution: { - width: r, - height: r, - pixelDepth: r, - colorDepth: r, - availableWidth: r, - availableHeight: r, - innerWidth: r, - innerHeight: r, - hasMultipleDisplays: r - }, - multimediaDevices: { - speakers: r, - microphones: r, - webcams: r - }, - mediaQueries: { - prefersColorScheme: r, - prefersReducedMotion: r, - prefersReducedTransparency: r, - colorGamut: r, - pointer: r, - anyPointer: r, - hover: r, - anyHover: r, - colorDepth: r - }, - keyboard: { - layout: r, - layoutSize: r - } - }, - // Browser identity & features - browser: { - userAgent: r, - features: { - bitmask: r, - chrome: r, - brave: r, - applePaySupport: r, - opera: r, - serial: r, - attachShadow: r, - caches: r, - webAssembly: r, - buffer: r, - showModalDialog: r, - safari: r, - webkitPrefixedFunction: r, - mozPrefixedFunction: r, - usb: r, - browserCapture: r, - paymentRequestUpdateEvent: r, - pressureObserver: r, - audioSession: r, - selectAudioOutput: r, - barcodeDetector: r, - battery: r, - devicePosture: r, - documentPictureInPicture: r, - eyeDropper: r, - editContext: r, - fencedFrame: r, - sanitizer: r, - otpCredential: r - }, - plugins: { - isValidPluginArray: r, - pluginCount: r, - pluginNamesHash: r, - pluginConsistency1: r, - pluginOverflow: r - }, - extensions: { - bitmask: r, - extensions: r - }, - highEntropyValues: { - architecture: r, - bitness: r, - brands: r, - mobile: r, - model: r, - platform: r, - platformVersion: r, - uaFullVersion: r - }, - etsl: r, - maths: r, - toSourceError: { - toSourceError: r, - hasToSource: r - }, - ai: { - summarizerAvailability: r, - summarizerLanguageAvailability: r - } - }, - // Graphics & rendering - graphics: { - webGL: { - vendor: r, - renderer: r - }, - webgpu: { - vendor: r, - architecture: r, - device: r, - description: r - }, - canvas: { - hasModifiedCanvas: r, - canvasFingerprint: r - } - }, - // Media codecs (at root level) - codecs: { - audioCanPlayTypeHash: r, - videoCanPlayTypeHash: r, - audioMediaSourceHash: r, - videoMediaSourceHash: r, - rtcAudioCapabilitiesHash: r, - rtcVideoCapabilitiesHash: r, - hasMediaSource: r - }, - // Locale & internationalization - locale: { - internationalization: { - timezone: r, - localeLanguage: r - }, - languages: { - languages: r, - language: r - } - }, - // Isolated execution contexts - contexts: { - iframe: { - webdriver: r, - userAgent: r, - platform: r, - memory: r, - cpuCount: r, - language: r - }, - webWorker: { - webdriver: r, - userAgent: r, - platform: r, - memory: r, - cpuCount: r, - language: r, - vendor: r, - renderer: r - } - } - }, - fsid: r, - nonce: r, - time: r, - url: r, - fastBotDetection: !1, - fastBotDetectionDetails: { - headlessChromeScreenResolution: { detected: !1, severity: "high" }, - hasWebdriver: { detected: !1, severity: "high" }, - hasWebdriverWritable: { detected: !1, severity: "high" }, - hasSeleniumProperty: { detected: !1, severity: "high" }, - hasCDP: { detected: !1, severity: "high" }, - hasPlaywright: { detected: !1, severity: "high" }, - hasImpossibleDeviceMemory: { detected: !1, severity: "high" }, - hasHighCPUCount: { detected: !1, severity: "high" }, - hasMissingChromeObject: { detected: !1, severity: "high" }, - hasWebdriverIframe: { detected: !1, severity: "high" }, - hasWebdriverWorker: { detected: !1, severity: "high" }, - hasMismatchWebGLInWorker: { detected: !1, severity: "high" }, - hasMismatchPlatformIframe: { detected: !1, severity: "high" }, - hasMismatchPlatformWorker: { detected: !1, severity: "high" }, - hasSwiftshaderRenderer: { detected: !1, severity: "low" }, - hasUTCTimezone: { detected: !1, severity: "medium" }, - hasMismatchLanguages: { detected: !1, severity: "low" }, - hasInconsistentEtsl: { detected: !1, severity: "high" }, - hasBotUserAgent: { detected: !1, severity: "high" }, - hasGPUMismatch: { detected: !1, severity: "high" }, - hasPlatformMismatch: { detected: !1, severity: "high" } - } - }; - } - async collectSignal(e) { - try { - return await e(); - } catch { - return l; - } - } - /** - * Generate a JA4-inspired fingerprint scanner ID - * Format: FS1________ - * - * Each section is delimited by '_', allowing partial matching. - * Sections use the pattern: h where applicable. - * Bitmasks are extensible - new boolean fields are appended without breaking existing positions. - * - * Sections: - * - det: fastBotDetectionDetails bitmask (21 bits: headlessChromeScreenResolution, hasWebdriver, - * hasWebdriverWritable, hasSeleniumProperty, hasCDP, hasPlaywright, hasImpossibleDeviceMemory, - * hasHighCPUCount, hasMissingChromeObject, hasWebdriverIframe, hasWebdriverWorker, - * hasMismatchWebGLInWorker, hasMismatchPlatformIframe, hasMismatchPlatformWorker, - * hasMismatchLanguages, hasInconsistentEtsl, hasBotUserAgent, hasGPUMismatch, hasPlatformMismatch) - * - auto: automation bitmask (5 bits: webdriver, webdriverWritable, selenium, cdp, playwright) + hash - * - dev: WIDTHxHEIGHT + cpu + mem + device bitmask + hash of all device signals - * - brw: features.bitmask + extensions.bitmask + plugins bitmask (3 bits) + hash of browser signals - * - gfx: canvas bitmask (1 bit: hasModifiedCanvas) + hash of all graphics signals - * - cod: codecs bitmask (1 bit: hasMediaSource) + hash of all codec hashes - * - loc: language code (2 chars) + language count + hash of locale signals - * - ctx: context mismatch bitmask (2 bits: iframe, worker) + hash of all context signals - */ - generateFingerprintScannerId() { - try { - const e = this.fingerprint.signals, n = this.fingerprint.fastBotDetectionDetails, i = "FS1", g = [ - n.headlessChromeScreenResolution.detected, - n.hasWebdriver.detected, - n.hasWebdriverWritable.detected, - n.hasSeleniumProperty.detected, - n.hasCDP.detected, - n.hasPlaywright.detected, - n.hasImpossibleDeviceMemory.detected, - n.hasHighCPUCount.detected, - n.hasMissingChromeObject.detected, - n.hasWebdriverIframe.detected, - n.hasWebdriverWorker.detected, - n.hasMismatchWebGLInWorker.detected, - n.hasMismatchPlatformIframe.detected, - n.hasMismatchPlatformWorker.detected, - n.hasSwiftshaderRenderer.detected, - n.hasUTCTimezone.detected, - n.hasMismatchLanguages.detected, - n.hasInconsistentEtsl.detected, - n.hasBotUserAgent.detected, - n.hasGPUMismatch.detected, - n.hasPlatformMismatch.detected - // Add other detection rules output here - ].map((u) => u ? "1" : "0").join(""), p = [ - e.automation.webdriver === !0, - e.automation.webdriverWritable === !0, - e.automation.selenium === !0, - e.automation.cdp === !0, - e.automation.playwright === !0 - ].map((u) => u ? "1" : "0").join(""), y = f(String(e.automation.navigatorPropertyDescriptors)).slice(0, 4), o = `${p}h${y}`, m = typeof e.device.screenResolution.width == "number" ? e.device.screenResolution.width : 0, w = typeof e.device.screenResolution.height == "number" ? e.device.screenResolution.height : 0, W = typeof e.device.cpuCount == "number" ? String(e.device.cpuCount).padStart(2, "0") : "00", E = typeof e.device.memory == "number" ? String(Math.round(e.device.memory)).padStart(2, "0") : "00", D = [ - e.device.screenResolution.hasMultipleDisplays === !0, - e.device.mediaQueries.prefersReducedMotion === !0, - e.device.mediaQueries.prefersReducedTransparency === !0, - e.device.mediaQueries.hover === !0, - e.device.mediaQueries.anyHover === !0 - ].map((u) => u ? "1" : "0").join(""), _ = [ - e.device.platform, - e.device.screenResolution.pixelDepth, - e.device.screenResolution.colorDepth, - e.device.multimediaDevices.speakers, - e.device.multimediaDevices.microphones, - e.device.multimediaDevices.webcams, - e.device.mediaQueries.prefersColorScheme, - e.device.mediaQueries.colorGamut, - e.device.mediaQueries.pointer, - e.device.mediaQueries.anyPointer, - e.device.mediaQueries.colorDepth, - e.device.keyboard.layout, - e.device.keyboard.layoutSize - ].map((u) => String(u)).join("|"), R = f(_).slice(0, 6), I = `${m}x${w}c${W}m${E}b${D}h${R}`, L = typeof e.browser.features.bitmask == "string" ? e.browser.features.bitmask : "0".repeat(29), T = typeof e.browser.extensions.bitmask == "string" ? e.browser.extensions.bitmask : "0".repeat(8), O = [ - e.browser.plugins.isValidPluginArray === !0, - e.browser.plugins.pluginConsistency1 === !0, - e.browser.plugins.pluginOverflow === !0, - e.browser.toSourceError.hasToSource === !0 - ].map((u) => u ? "1" : "0").join(""), H = [ - e.browser.userAgent, - e.browser.etsl, - e.browser.maths, - e.browser.plugins.pluginCount, - e.browser.plugins.pluginNamesHash, - e.browser.toSourceError.toSourceError, - e.browser.highEntropyValues.architecture, - e.browser.highEntropyValues.bitness, - e.browser.highEntropyValues.platform, - e.browser.highEntropyValues.platformVersion, - e.browser.highEntropyValues.uaFullVersion, - e.browser.highEntropyValues.mobile, - e.browser.ai.summarizerAvailability, - e.browser.ai.summarizerLanguageAvailability - ].map((u) => String(u)).join("|"), z = f(H).slice(0, 6), U = `f${L}e${T}p${O}h${z}`, N = [ - e.graphics.canvas.hasModifiedCanvas === !0 - ].map((u) => u ? "1" : "0").join(""), F = [ - e.graphics.webGL.vendor, - e.graphics.webGL.renderer, - e.graphics.webgpu.vendor, - e.graphics.webgpu.architecture, - e.graphics.webgpu.device, - e.graphics.webgpu.description, - e.graphics.canvas.canvasFingerprint - ].map((u) => String(u)).join("|"), G = f(F).slice(0, 6), V = `${N}h${G}`, B = [ - e.codecs.hasMediaSource === !0 - ].map((u) => u ? "1" : "0").join(""), j = [ - e.codecs.audioCanPlayTypeHash, - e.codecs.videoCanPlayTypeHash, - e.codecs.audioMediaSourceHash, - e.codecs.videoMediaSourceHash, - e.codecs.rtcAudioCapabilitiesHash, - e.codecs.rtcVideoCapabilitiesHash - ].map((u) => String(u)).join("|"), $ = f(j).slice(0, 6), Q = `${B}h${$}`, q = typeof e.locale.languages.language == "string" ? e.locale.languages.language.slice(0, 2).toLowerCase() : "xx", K = Array.isArray(e.locale.languages.languages) ? e.locale.languages.languages.length : 0, J = (typeof e.locale.internationalization.timezone == "string" ? e.locale.internationalization.timezone : "unknown").replace(/[\/\s]/g, "-"), Y = [ - e.locale.internationalization.timezone, - e.locale.internationalization.localeLanguage, - Array.isArray(e.locale.languages.languages) ? e.locale.languages.languages.join(",") : e.locale.languages.languages, - e.locale.languages.language - ].map((u) => String(u)).join("|"), Z = f(Y).slice(0, 4), X = `${q}${K}t${J}_h${Z}`, ee = [ - x(this.fingerprint, "iframe"), - x(this.fingerprint, "worker"), - e.contexts.iframe.webdriver === !0, - e.contexts.webWorker.webdriver === !0 - ].map((u) => u ? "1" : "0").join(""), te = [ - e.contexts.iframe.userAgent, - e.contexts.iframe.platform, - e.contexts.iframe.memory, - e.contexts.iframe.cpuCount, - e.contexts.iframe.language, - e.contexts.webWorker.userAgent, - e.contexts.webWorker.platform, - e.contexts.webWorker.memory, - e.contexts.webWorker.cpuCount, - e.contexts.webWorker.language, - e.contexts.webWorker.vendor, - e.contexts.webWorker.renderer - ].map((u) => String(u)).join("|"), re = f(te).slice(0, 6), ne = `${ee}h${re}`; - return [ - i, - g, - o, - I, - U, - V, - Q, - X, - ne - ].join("_"); - } catch (e) { - return console.error("Error generating fingerprint scanner id", e), l; - } - } - async encryptFingerprint(e) { - const n = "__DEFAULT_FPSCANNER_KEY__"; - return n.length > 20 && n.indexOf("DEFAULT") > 0 && n.indexOf("FPSCANNER") > 0 && console.warn( - '[fpscanner] WARNING: Using default encryption key! Run "npx fpscanner build --key=your-secret-key" to inject your own key. See: https://github.com/antoinevastel/fpscanner#advanced-custom-builds' - ), await pt(JSON.stringify(e), n); - } - /** - * Detection rules with name and severity. - */ - getDetectionRules() { - return [ - { name: "headlessChromeScreenResolution", severity: h, test: $e }, - { name: "hasWebdriver", severity: h, test: Qe }, - { name: "hasWebdriverWritable", severity: h, test: st }, - { name: "hasSeleniumProperty", severity: h, test: qe }, - { name: "hasCDP", severity: h, test: Ke }, - { name: "hasPlaywright", severity: h, test: Je }, - { name: "hasImpossibleDeviceMemory", severity: h, test: Ye }, - { name: "hasHighCPUCount", severity: h, test: Ze }, - { name: "hasMissingChromeObject", severity: h, test: et }, - { name: "hasWebdriverIframe", severity: h, test: tt }, - { name: "hasWebdriverWorker", severity: h, test: rt }, - { name: "hasMismatchWebGLInWorker", severity: h, test: nt }, - { name: "hasMismatchPlatformIframe", severity: h, test: ot }, - { name: "hasMismatchPlatformWorker", severity: h, test: at }, - { name: "hasSwiftshaderRenderer", severity: S, test: ct }, - { name: "hasUTCTimezone", severity: se, test: lt }, - { name: "hasMismatchLanguages", severity: S, test: ut }, - { name: "hasInconsistentEtsl", severity: h, test: dt }, - { name: "hasBotUserAgent", severity: h, test: gt }, - { name: "hasGPUMismatch", severity: h, test: ht }, - { name: "hasPlatformMismatch", severity: h, test: mt } - ]; - } - runDetectionRules() { - const e = this.getDetectionRules(), n = { - headlessChromeScreenResolution: { detected: !1, severity: "high" }, - hasWebdriver: { detected: !1, severity: "high" }, - hasWebdriverWritable: { detected: !1, severity: "high" }, - hasSeleniumProperty: { detected: !1, severity: "high" }, - hasCDP: { detected: !1, severity: "high" }, - hasPlaywright: { detected: !1, severity: "high" }, - hasImpossibleDeviceMemory: { detected: !1, severity: "high" }, - hasHighCPUCount: { detected: !1, severity: "high" }, - hasMissingChromeObject: { detected: !1, severity: "high" }, - hasWebdriverIframe: { detected: !1, severity: "high" }, - hasWebdriverWorker: { detected: !1, severity: "high" }, - hasMismatchWebGLInWorker: { detected: !1, severity: "high" }, - hasMismatchPlatformIframe: { detected: !1, severity: "high" }, - hasMismatchPlatformWorker: { detected: !1, severity: "high" }, - hasSwiftshaderRenderer: { detected: !1, severity: "low" }, - hasUTCTimezone: { detected: !1, severity: "medium" }, - hasMismatchLanguages: { detected: !1, severity: "low" }, - hasInconsistentEtsl: { detected: !1, severity: "high" }, - hasBotUserAgent: { detected: !1, severity: "high" }, - hasGPUMismatch: { detected: !1, severity: "high" }, - hasPlatformMismatch: { detected: !1, severity: "high" } - }; - for (const i of e) - try { - const a = i.test(this.fingerprint); - n[i.name] = { detected: a, severity: i.severity }; - } catch { - n[i.name] = { detected: !1, severity: i.severity }; - } - return n; - } - async collectFingerprint(e = { encrypt: !0 }) { - const { encrypt: n = !0, skipWorker: i = !1 } = e, a = this.fingerprint.signals, g = { - // Automation signals - webdriver: this.collectSignal(ie), - webdriverWritable: this.collectSignal(Se), - selenium: this.collectSignal(be), - cdp: this.collectSignal(le), - playwright: this.collectSignal(de), - navigatorPropertyDescriptors: this.collectSignal(He), - // Device signals - cpuCount: this.collectSignal(ge), - memory: this.collectSignal(me), - platform: this.collectSignal(oe), - screenResolution: this.collectSignal(ve), - multimediaDevices: this.collectSignal(Ee), - mediaQueries: this.collectSignal(Ve), - keyboard: this.collectSignal(Be), - // Browser signals - userAgent: this.collectSignal(ae), - browserFeatures: this.collectSignal(Ge), - plugins: this.collectSignal(We), - browserExtensions: this.collectSignal(Fe), - highEntropyValues: this.collectSignal(Ce), - etsl: this.collectSignal(pe), - maths: this.collectSignal(he), - toSourceError: this.collectSignal(Re), - ai: this.collectSignal(je), - // Graphics signals - webGL: this.collectSignal(ue), - webgpu: this.collectSignal(we), - canvas: this.collectSignal(Oe), - // Codecs - mediaCodecs: this.collectSignal(Ie), - // Locale signals - internationalization: this.collectSignal(fe), - languages: this.collectSignal(ye), - // Context signals - iframe: this.collectSignal(De), - webWorker: i ? Promise.resolve({ - webdriver: v, - userAgent: v, - platform: v, - memory: v, - cpuCount: v, - language: v, - vendor: v, - renderer: v - }) : this.collectSignal(_e), - // Meta signals - nonce: this.collectSignal(ze), - time: this.collectSignal(Ue), - url: this.collectSignal(Ne) - }, p = Object.keys(g), y = await Promise.all(Object.values(g)), o = Object.fromEntries(p.map((m, w) => [m, y[w]])); - return a.automation.webdriver = o.webdriver, a.automation.webdriverWritable = o.webdriverWritable, a.automation.selenium = o.selenium, a.automation.cdp = o.cdp, a.automation.playwright = o.playwright, a.automation.navigatorPropertyDescriptors = o.navigatorPropertyDescriptors, a.device.cpuCount = o.cpuCount, a.device.memory = o.memory, a.device.platform = o.platform, a.device.screenResolution = o.screenResolution, a.device.multimediaDevices = o.multimediaDevices, a.device.mediaQueries = o.mediaQueries, a.device.keyboard = o.keyboard, a.browser.userAgent = o.userAgent, a.browser.features = o.browserFeatures, a.browser.plugins = o.plugins, a.browser.extensions = o.browserExtensions, a.browser.highEntropyValues = o.highEntropyValues, a.browser.etsl = o.etsl, a.browser.maths = o.maths, a.browser.toSourceError = o.toSourceError, a.browser.ai = o.ai, a.graphics.webGL = o.webGL, a.graphics.webgpu = o.webgpu, a.graphics.canvas = o.canvas, a.codecs = o.mediaCodecs, a.locale.internationalization = o.internationalization, a.locale.languages = o.languages, a.contexts.iframe = o.iframe, a.contexts.webWorker = o.webWorker, this.fingerprint.nonce = o.nonce, this.fingerprint.time = o.time, this.fingerprint.url = o.url, this.fingerprint.fastBotDetectionDetails = this.runDetectionRules(), this.fingerprint.fastBotDetection = Object.values(this.fingerprint.fastBotDetectionDetails).some((m) => m.detected), this.fingerprint.fsid = this.generateFingerprintScannerId(), n ? await this.encryptFingerprint(JSON.stringify(this.fingerprint)) : this.fingerprint; - } -} -export { - vt as default -};