feat: initial public release
invisible-playwright: a patched Firefox 150.0.1 for browser-fingerprint
stealth, shipped as a Playwright-compatible Python wrapper.
* Sync + async InvisiblePlaywright launcher (firefox_user_prefs, virtual
desktop on Windows, SOCKS5 auth via patched nsProtocolProxyService)
* fpforge: Bayesian fingerprint sampler over GPU / audio / fonts /
screen / ~400 other navigator fields
* WebRTC stealth: srflx address swap, synthetic srflx fallback,
private-LAN host candidates. No real public IP leak via STUN.
* GPU sandbox fix for FF150 alt-desktop regression
* Bezier-curve mouse motion baked into Juggler
Targets Windows x86_64 + Linux x86_64. Binary fetched on first run from
GitHub Release "firefox-1".
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
|
||||
def test_version_subcommand():
|
||||
r = subprocess.run(
|
||||
[sys.executable, "-m", "invisible-playwright", "version"],
|
||||
capture_output=True, text=True, check=True,
|
||||
)
|
||||
assert "firefox-" in r.stdout
|
||||
assert "invisible-playwright" in r.stdout.lower()
|
||||
|
||||
|
||||
def test_help_subcommand():
|
||||
r = subprocess.run(
|
||||
[sys.executable, "-m", "invisible-playwright", "--help"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
assert r.returncode == 0
|
||||
assert "fetch" in r.stdout
|
||||
assert "path" in r.stdout
|
||||
assert "clear-cache" in r.stdout
|
||||
@@ -0,0 +1,29 @@
|
||||
from invisible_playwright.constants import BINARY_VERSION, BINARY_BASENAME, ARCHIVE_NAME
|
||||
|
||||
|
||||
def test_binary_version_format():
|
||||
assert BINARY_VERSION.startswith("firefox-")
|
||||
assert BINARY_VERSION.split("-", 1)[1].isdigit()
|
||||
|
||||
|
||||
def test_archive_name_windows():
|
||||
name = ARCHIVE_NAME("win32", "AMD64")
|
||||
assert name.endswith(".zip")
|
||||
assert "win-x86_64" in name
|
||||
|
||||
|
||||
def test_archive_name_linux():
|
||||
name = ARCHIVE_NAME("linux", "x86_64")
|
||||
assert name.endswith(".tar.gz")
|
||||
assert "linux-x86_64" in name
|
||||
|
||||
|
||||
def test_archive_name_unsupported_raises():
|
||||
import pytest
|
||||
with pytest.raises(NotImplementedError):
|
||||
ARCHIVE_NAME("darwin", "arm64")
|
||||
|
||||
|
||||
def test_binary_basename_format():
|
||||
assert "firefox" in BINARY_BASENAME.lower()
|
||||
assert "stealth" in BINARY_BASENAME.lower()
|
||||
@@ -0,0 +1,71 @@
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import responses
|
||||
|
||||
from invisible_playwright.download import ensure_binary
|
||||
from invisible_playwright.constants import BINARY_VERSION
|
||||
|
||||
|
||||
def _make_zip(path: Path, inner_name: str, payload: bytes) -> bytes:
|
||||
import io
|
||||
import zipfile
|
||||
buf = io.BytesIO()
|
||||
with zipfile.ZipFile(buf, "w") as zf:
|
||||
zf.writestr(inner_name, payload)
|
||||
data = buf.getvalue()
|
||||
path.write_bytes(data)
|
||||
return data
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_ensure_binary_downloads_and_verifies(tmp_path, monkeypatch):
|
||||
"""Full path: cache miss -> HTTP GET -> SHA256 check -> extract -> return path."""
|
||||
cache = tmp_path / "cache"
|
||||
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
||||
|
||||
archive_path = tmp_path / "archive.zip"
|
||||
archive_bytes = _make_zip(archive_path, "firefox.exe", b"PEX!")
|
||||
archive_sha = hashlib.sha256(archive_bytes).hexdigest()
|
||||
from invisible_playwright.constants import ARCHIVE_NAME
|
||||
asset = ARCHIVE_NAME("win32", "AMD64")
|
||||
|
||||
url_archive = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/{asset}"
|
||||
url_sums = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/checksums.txt"
|
||||
|
||||
responses.add(responses.GET, url_archive, body=archive_bytes, status=200,
|
||||
content_type="application/zip")
|
||||
responses.add(responses.GET, url_sums,
|
||||
body=f"{archive_sha} {asset}\n", status=200)
|
||||
|
||||
monkeypatch.setattr("sys.platform", "win32")
|
||||
import platform
|
||||
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
|
||||
|
||||
path = ensure_binary()
|
||||
assert Path(path).exists()
|
||||
assert Path(path).name == "firefox.exe"
|
||||
|
||||
|
||||
@responses.activate
|
||||
def test_ensure_binary_rejects_sha_mismatch(tmp_path, monkeypatch):
|
||||
cache = tmp_path / "cache"
|
||||
monkeypatch.setattr("invisible_playwright.download.cache_root", lambda: cache)
|
||||
archive_path = tmp_path / "archive.zip"
|
||||
archive_bytes = _make_zip(archive_path, "firefox.exe", b"PEX!")
|
||||
wrong_sha = "0" * 64
|
||||
from invisible_playwright.constants import ARCHIVE_NAME
|
||||
asset = ARCHIVE_NAME("win32", "AMD64")
|
||||
|
||||
url_archive = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/{asset}"
|
||||
url_sums = f"https://github.com/feder-cr/invisible_playwright/releases/download/{BINARY_VERSION}/checksums.txt"
|
||||
responses.add(responses.GET, url_archive, body=archive_bytes, status=200)
|
||||
responses.add(responses.GET, url_sums, body=f"{wrong_sha} {asset}\n", status=200)
|
||||
|
||||
monkeypatch.setattr("sys.platform", "win32")
|
||||
import platform
|
||||
monkeypatch.setattr(platform, "machine", lambda: "AMD64")
|
||||
|
||||
with pytest.raises(RuntimeError, match="SHA256"):
|
||||
ensure_binary()
|
||||
@@ -0,0 +1,35 @@
|
||||
from invisible_playwright._fpforge import generate_profile
|
||||
from invisible_playwright.prefs import translate_profile_to_prefs
|
||||
|
||||
|
||||
def test_translate_includes_gpu_renderer():
|
||||
p = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(p)
|
||||
assert prefs["zoom.stealth.webgl.renderer"] == p.gpu.renderer
|
||||
assert prefs["zoom.stealth.webgl.vendor"] == p.gpu.vendor
|
||||
|
||||
|
||||
def test_translate_includes_screen():
|
||||
p = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(p)
|
||||
assert prefs["zoom.stealth.screen.width"] == p.screen.width
|
||||
assert prefs["zoom.stealth.screen.height"] == p.screen.height
|
||||
|
||||
|
||||
def test_translate_is_deterministic_per_seed():
|
||||
a = translate_profile_to_prefs(generate_profile(seed=42))
|
||||
b = translate_profile_to_prefs(generate_profile(seed=42))
|
||||
assert a == b
|
||||
|
||||
|
||||
def test_translate_varies_across_seeds():
|
||||
a = translate_profile_to_prefs(generate_profile(seed=1))
|
||||
b = translate_profile_to_prefs(generate_profile(seed=2))
|
||||
assert a != b
|
||||
|
||||
|
||||
def test_translate_has_stealth_baseline_constants():
|
||||
p = generate_profile(seed=42)
|
||||
prefs = translate_profile_to_prefs(p)
|
||||
assert prefs.get("privacy.resistFingerprinting") is False
|
||||
assert "media.peerconnection.enabled" in prefs
|
||||
Reference in New Issue
Block a user