Files
CloakBrowser/cloakbrowser/config.py
T
CloakHQ 50bf14b3f9 fix(wrapper): track real window geometry on headed launches
Headed launches applied a fixed emulated viewport on top of the real
browser window, yielding outerWidth < innerWidth (an impossible window).
Default headed new_page()/new_context() to no_viewport so the page tracks
the real window; headless keeps a deterministic viewport. Covers Python
launch/launch_context/launch_persistent_context (+async) and the JS
Playwright/Puppeteer wrappers. Explicit viewport still honored.
2026-06-20 22:53:53 +02:00

242 lines
9.3 KiB
Python

"""Stealth configuration and platform detection for cloakbrowser."""
from __future__ import annotations
import os
import platform
import random
from pathlib import Path
from ._version import __version__
# ---------------------------------------------------------------------------
# Chromium version shipped with this release.
# Different platforms may ship different versions during transition periods.
# CHROMIUM_VERSION is the latest across all platforms (for display/reference).
# Use get_chromium_version() for the current platform's actual version.
# ---------------------------------------------------------------------------
CHROMIUM_VERSION = "146.0.7680.177.5"
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
"linux-x64": "146.0.7680.177.5",
"linux-arm64": "146.0.7680.177.3",
"darwin-arm64": "145.0.7632.109.2",
"darwin-x64": "145.0.7632.109.2",
"windows-x64": "146.0.7680.177.5",
}
# ---------------------------------------------------------------------------
# Playwright default args to suppress — these leak automation signals.
# --enable-automation: exposes navigator.webdriver = true
# --enable-unsafe-swiftshader: forces software WebGL rendering via SwiftShader,
# producing a distinctive renderer string that no real user browser has
# ---------------------------------------------------------------------------
IGNORE_DEFAULT_ARGS = ["--enable-automation", "--enable-unsafe-swiftshader"]
# ---------------------------------------------------------------------------
# Default stealth arguments passed to the patched Chromium binary.
# These activate source-level fingerprint patches compiled into the binary.
# ---------------------------------------------------------------------------
def get_default_stealth_args() -> list[str]:
"""Build stealth args with a random fingerprint seed per launch.
On macOS, skips platform/GPU spoofing — runs as a native Mac browser.
Spoofing Windows on Mac creates detectable mismatches (fonts, GPU, etc.).
"""
seed = random.randint(10000, 99999)
system = platform.system()
base = [
"--no-sandbox",
f"--fingerprint={seed}",
]
if system == "Darwin":
# Tell the fingerprint patches we're on macOS so GPU/UA match natively
return base + ["--fingerprint-platform=macos"]
# Linux/Windows: Windows fingerprint profile.
# Screen and window size come from the real display, not this flag (verified:
# identical across seeds), so the wrapper must not emulate a viewport on top in
# headed mode — that would break outerWidth >= innerWidth coherence.
return base + ["--fingerprint-platform=windows"]
# ---------------------------------------------------------------------------
# Default viewport — used for HEADLESS only (headed launches use no_viewport so
# the page tracks the real window). Headless has no window chrome, so a fixed
# viewport stays coherent (outer == inner) and gives deterministic dimensions.
# Models a maximized Chrome on 1080p Windows: screen=1920x1080,
# innerHeight=947 (minus ~85px Chrome UI: tabs + address bar + bookmarks).
# ---------------------------------------------------------------------------
DEFAULT_VIEWPORT = {"width": 1920, "height": 947}
# ---------------------------------------------------------------------------
# Platform detection
# ---------------------------------------------------------------------------
SUPPORTED_PLATFORMS: dict[tuple[str, str], str] = {
("Linux", "x86_64"): "linux-x64",
("Linux", "aarch64"): "linux-arm64",
("Darwin", "arm64"): "darwin-arm64",
("Darwin", "x86_64"): "darwin-x64",
("Windows", "AMD64"): "windows-x64",
("Windows", "x86_64"): "windows-x64",
}
# Platforms with pre-built binaries available for download (derived from version map).
AVAILABLE_PLATFORMS: set[str] = set(PLATFORM_CHROMIUM_VERSIONS.keys())
def get_chromium_version() -> str:
"""Return the Chromium version for the current platform."""
tag = get_platform_tag()
return PLATFORM_CHROMIUM_VERSIONS.get(tag, CHROMIUM_VERSION)
def get_platform_tag() -> str:
"""Return the platform tag for binary download (e.g. 'linux-x64', 'darwin-arm64')."""
system = platform.system()
machine = platform.machine()
tag = SUPPORTED_PLATFORMS.get((system, machine))
if tag is None:
raise RuntimeError(
f"Unsupported platform: {system} {machine}. "
f"Supported: {', '.join(f'{s}-{m}' for (s, m) in SUPPORTED_PLATFORMS)}"
)
return tag
# ---------------------------------------------------------------------------
# Binary cache paths
# ---------------------------------------------------------------------------
def get_cache_dir() -> Path:
"""Return the cache directory for downloaded binaries.
Override with CLOAKBROWSER_CACHE_DIR env var.
Default: ~/.cloakbrowser/
"""
custom = os.environ.get("CLOAKBROWSER_CACHE_DIR")
if custom:
return Path(custom)
return Path.home() / ".cloakbrowser"
def get_binary_dir(version: str | None = None) -> Path:
"""Return the directory for a Chromium version binary."""
v = version or get_chromium_version()
return get_cache_dir() / f"chromium-{v}"
def get_binary_path(version: str | None = None) -> Path:
"""Return the expected path to the chrome executable."""
binary_dir = get_binary_dir(version)
if platform.system() == "Darwin":
# macOS: Chromium.app bundle
return binary_dir / "Chromium.app" / "Contents" / "MacOS" / "Chromium"
elif platform.system() == "Windows":
return binary_dir / "chrome.exe"
else:
# Linux: flat binary
return binary_dir / "chrome"
def check_platform_available() -> None:
"""Raise a clear error if no pre-built binary exists for this platform.
Skipped when CLOAKBROWSER_BINARY_PATH is set (user has their own build).
"""
if get_local_binary_override():
return
tag = get_platform_tag() # raises if platform unsupported entirely
if tag not in AVAILABLE_PLATFORMS:
available = ", ".join(sorted(AVAILABLE_PLATFORMS))
import sys
sys.exit(
f"\n\033[1mCloakBrowser\033[0m — Pre-built binaries are currently only available for: {available}.\n\n"
f"To use CloakBrowser now, set CLOAKBROWSER_BINARY_PATH to a local Chromium binary."
)
def get_effective_version() -> str:
"""Return the best available version: auto-updated if available, else platform default.
Reads a platform-scoped marker file from the cache directory.
Returns the platform's hardcoded version if no update has been downloaded.
"""
base = get_chromium_version()
# Try platform-scoped marker first, fall back to legacy marker for upgrades from <0.3.0
cache = get_cache_dir()
for name in (f"latest_version_{get_platform_tag()}", "latest_version"):
marker = cache / name
if marker.exists():
try:
version = marker.read_text().strip()
if version and _version_newer(version, base):
binary = get_binary_path(version)
if binary.exists():
return version
except (ValueError, OSError):
pass
return base
def _version_tuple(v: str) -> tuple[int, ...]:
"""Parse '145.0.7718.0' into (145, 0, 7718, 0) for comparison."""
return tuple(int(x) for x in v.split("."))
def _version_newer(a: str, b: str) -> bool:
"""Return True if version a is strictly newer than version b."""
return _version_tuple(a) > _version_tuple(b)
# ---------------------------------------------------------------------------
# Download URL
# ---------------------------------------------------------------------------
DOWNLOAD_BASE_URL = os.environ.get(
"CLOAKBROWSER_DOWNLOAD_URL",
"https://cloakbrowser.dev",
)
GITHUB_API_URL = "https://api.github.com/repos/CloakHQ/cloakbrowser/releases"
GITHUB_DOWNLOAD_BASE_URL = (
"https://github.com/CloakHQ/cloakbrowser/releases/download"
)
def get_archive_ext() -> str:
"""Return the archive extension for the current platform (.zip for Windows, .tar.gz otherwise)."""
return ".zip" if platform.system() == "Windows" else ".tar.gz"
def get_archive_name(tag: str | None = None) -> str:
"""Return the archive filename for a platform tag (e.g. 'cloakbrowser-linux-x64.tar.gz')."""
t = tag or get_platform_tag()
return f"cloakbrowser-{t}{get_archive_ext()}"
def get_download_url(version: str | None = None) -> str:
"""Return the full download URL for the current platform's binary archive."""
v = version or get_chromium_version()
return f"{DOWNLOAD_BASE_URL}/chromium-v{v}/{get_archive_name()}"
def get_fallback_download_url(version: str | None = None) -> str:
"""Return the GitHub Releases fallback URL for the binary archive."""
v = version or get_chromium_version()
return f"{GITHUB_DOWNLOAD_BASE_URL}/chromium-v{v}/{get_archive_name()}"
# ---------------------------------------------------------------------------
# Local binary override (skip download, use your own build)
# ---------------------------------------------------------------------------
def get_local_binary_override() -> str | None:
"""Check if user has set a local binary path via env var.
Set CLOAKBROWSER_BINARY_PATH to use a locally built Chromium instead of downloading.
"""
return os.environ.get("CLOAKBROWSER_BINARY_PATH")