"""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")