mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
feat: move binary releases to wrapper repo, add auto-update check
- Binary downloads now served from CloakHQ/cloakbrowser releases (chromium-v* tags) - Auto-update: background version check on launch, downloads newer binary for next use - Graceful error on macOS/Windows (Linux-only binaries for now) - Rate-limited (1hr), opt-out via CLOAKBROWSER_AUTO_UPDATE=false - Add release-binary.yml workflow for anonymous binary releases
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
name: Release Binary
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Release tag (e.g. chromium-v145.0.7718.0)'
|
||||||
|
required: true
|
||||||
|
platform:
|
||||||
|
description: 'Platform label (e.g. Linux x64)'
|
||||||
|
required: true
|
||||||
|
default: 'Linux x64'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Create release
|
||||||
|
uses: softprops/action-gh-release@v2
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.event.inputs.tag }}
|
||||||
|
name: "${{ github.event.inputs.platform }} — ${{ github.event.inputs.tag }}"
|
||||||
|
body: |
|
||||||
|
## Stealth Chromium Build
|
||||||
|
|
||||||
|
Pre-built Chromium with 16 source-level fingerprint patches.
|
||||||
|
|
||||||
|
### Install
|
||||||
|
```bash
|
||||||
|
pip install cloakbrowser # Python
|
||||||
|
npm install cloakbrowser # JavaScript
|
||||||
|
# Binary auto-downloads on first launch
|
||||||
|
```
|
||||||
|
|
||||||
|
### Platforms
|
||||||
|
- ${{ github.event.inputs.platform }}
|
||||||
@@ -57,3 +57,4 @@ test-infra/
|
|||||||
publish.sh
|
publish.sh
|
||||||
deploy.sh
|
deploy.sh
|
||||||
.env
|
.env
|
||||||
|
debug
|
||||||
|
|||||||
@@ -284,6 +284,7 @@ clearCache();
|
|||||||
| `CLOAKBROWSER_BINARY_PATH` | — | Skip download, use a local Chromium binary |
|
| `CLOAKBROWSER_BINARY_PATH` | — | Skip download, use a local Chromium binary |
|
||||||
| `CLOAKBROWSER_CACHE_DIR` | `~/.cloakbrowser` | Binary cache directory |
|
| `CLOAKBROWSER_CACHE_DIR` | `~/.cloakbrowser` | Binary cache directory |
|
||||||
| `CLOAKBROWSER_DOWNLOAD_URL` | GitHub Releases | Custom download URL for binary |
|
| `CLOAKBROWSER_DOWNLOAD_URL` | GitHub Releases | Custom download URL for binary |
|
||||||
|
| `CLOAKBROWSER_AUTO_UPDATE` | `true` | Set to `false` to disable background update checks |
|
||||||
|
|
||||||
## Use With Existing Playwright Code
|
## Use With Existing Playwright Code
|
||||||
|
|
||||||
@@ -315,13 +316,17 @@ page.goto("https://example.com")
|
|||||||
|
|
||||||
## Platforms
|
## Platforms
|
||||||
|
|
||||||
|
> **CloakBrowser is in active development.** Pre-built binaries are currently Linux-only. macOS and Windows builds are coming soon.
|
||||||
|
|
||||||
| Platform | Status |
|
| Platform | Status |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Linux x86_64 | Supported |
|
| Linux x86_64 | ✅ Available |
|
||||||
| macOS arm64 (Apple Silicon) | Coming soon |
|
| macOS arm64 (Apple Silicon) | Coming soon |
|
||||||
| macOS x86_64 (Intel) | Coming soon |
|
| macOS x86_64 (Intel) | Coming soon |
|
||||||
| Windows | Planned |
|
| Windows | Planned |
|
||||||
|
|
||||||
|
**On macOS/Windows?** You can still use CloakBrowser via Docker or with your own Chromium binary by setting `CLOAKBROWSER_BINARY_PATH=/path/to/chrome`.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
**Python** — see [`examples/`](examples/):
|
**Python** — see [`examples/`](examples/):
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Usage:
|
|||||||
|
|
||||||
from .browser import launch, launch_async, launch_context
|
from .browser import launch, launch_async, launch_context
|
||||||
from .config import CHROMIUM_VERSION, get_default_stealth_args
|
from .config import CHROMIUM_VERSION, get_default_stealth_args
|
||||||
from .download import binary_info, clear_cache, ensure_binary
|
from .download import binary_info, check_for_update, clear_cache, ensure_binary
|
||||||
from ._version import __version__
|
from ._version import __version__
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
@@ -23,6 +23,7 @@ __all__ = [
|
|||||||
"ensure_binary",
|
"ensure_binary",
|
||||||
"clear_cache",
|
"clear_cache",
|
||||||
"binary_info",
|
"binary_info",
|
||||||
|
"check_for_update",
|
||||||
"CHROMIUM_VERSION",
|
"CHROMIUM_VERSION",
|
||||||
"get_default_stealth_args",
|
"get_default_stealth_args",
|
||||||
"__version__",
|
"__version__",
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.1.6"
|
__version__ = "0.1.7"
|
||||||
|
|||||||
+65
-9
@@ -41,6 +41,10 @@ SUPPORTED_PLATFORMS: dict[tuple[str, str], str] = {
|
|||||||
("Darwin", "x86_64"): "darwin-x64",
|
("Darwin", "x86_64"): "darwin-x64",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Platforms with pre-built binaries available for download.
|
||||||
|
# Update this set as new platform builds are released.
|
||||||
|
AVAILABLE_PLATFORMS: set[str] = {"linux-x64"}
|
||||||
|
|
||||||
|
|
||||||
def get_platform_tag() -> str:
|
def get_platform_tag() -> str:
|
||||||
"""Return the platform tag for binary download (e.g. 'linux-x64', 'darwin-arm64')."""
|
"""Return the platform tag for binary download (e.g. 'linux-x64', 'darwin-arm64')."""
|
||||||
@@ -70,15 +74,15 @@ def get_cache_dir() -> Path:
|
|||||||
return Path.home() / ".cloakbrowser"
|
return Path.home() / ".cloakbrowser"
|
||||||
|
|
||||||
|
|
||||||
def get_binary_dir() -> Path:
|
def get_binary_dir(version: str | None = None) -> Path:
|
||||||
"""Return the directory for the current Chromium version binary."""
|
"""Return the directory for a Chromium version binary."""
|
||||||
return get_cache_dir() / f"chromium-{CHROMIUM_VERSION}"
|
v = version or CHROMIUM_VERSION
|
||||||
|
return get_cache_dir() / f"chromium-{v}"
|
||||||
|
|
||||||
|
|
||||||
def get_binary_path() -> Path:
|
def get_binary_path(version: str | None = None) -> Path:
|
||||||
"""Return the expected path to the chrome executable."""
|
"""Return the expected path to the chrome executable."""
|
||||||
platform_tag = get_platform_tag()
|
binary_dir = get_binary_dir(version)
|
||||||
binary_dir = get_binary_dir()
|
|
||||||
|
|
||||||
if platform.system() == "Darwin":
|
if platform.system() == "Darwin":
|
||||||
# macOS: Chromium.app bundle
|
# macOS: Chromium.app bundle
|
||||||
@@ -88,19 +92,71 @@ def get_binary_path() -> Path:
|
|||||||
return binary_dir / "chrome"
|
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"
|
||||||
|
f"macOS and Windows builds are coming soon.\n\n"
|
||||||
|
f"To use CloakBrowser now, run in Docker (see README)."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_effective_version() -> str:
|
||||||
|
"""Return the best available version: auto-updated if available, else hardcoded.
|
||||||
|
|
||||||
|
Reads the latest_version marker file from the cache directory.
|
||||||
|
Returns CHROMIUM_VERSION if no update has been downloaded.
|
||||||
|
"""
|
||||||
|
marker = get_cache_dir() / "latest_version"
|
||||||
|
if marker.exists():
|
||||||
|
try:
|
||||||
|
version = marker.read_text().strip()
|
||||||
|
if version and _version_newer(version, CHROMIUM_VERSION):
|
||||||
|
# Verify the binary actually exists
|
||||||
|
binary = get_binary_path(version)
|
||||||
|
if binary.exists():
|
||||||
|
return version
|
||||||
|
except (ValueError, OSError):
|
||||||
|
pass
|
||||||
|
return CHROMIUM_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
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 URL
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
DOWNLOAD_BASE_URL = os.environ.get(
|
DOWNLOAD_BASE_URL = os.environ.get(
|
||||||
"CLOAKBROWSER_DOWNLOAD_URL",
|
"CLOAKBROWSER_DOWNLOAD_URL",
|
||||||
"https://github.com/CloakHQ/chromium-stealth-builds/releases/download",
|
"https://github.com/CloakHQ/cloakbrowser/releases/download",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
GITHUB_API_URL = "https://api.github.com/repos/CloakHQ/cloakbrowser/releases"
|
||||||
|
|
||||||
def get_download_url() -> str:
|
|
||||||
|
def get_download_url(version: str | None = None) -> str:
|
||||||
"""Return the full download URL for the current platform's binary archive."""
|
"""Return the full download URL for the current platform's binary archive."""
|
||||||
|
v = version or CHROMIUM_VERSION
|
||||||
tag = get_platform_tag()
|
tag = get_platform_tag()
|
||||||
return f"{DOWNLOAD_BASE_URL}/v{CHROMIUM_VERSION}/cloakbrowser-{tag}.tar.gz"
|
return f"{DOWNLOAD_BASE_URL}/chromium-v{v}/cloakbrowser-{tag}.tar.gz"
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
+167
-17
@@ -11,15 +11,23 @@ import os
|
|||||||
import stat
|
import stat
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from .config import (
|
from .config import (
|
||||||
CHROMIUM_VERSION,
|
CHROMIUM_VERSION,
|
||||||
|
DOWNLOAD_BASE_URL,
|
||||||
|
GITHUB_API_URL,
|
||||||
|
_version_newer,
|
||||||
|
check_platform_available,
|
||||||
get_binary_dir,
|
get_binary_dir,
|
||||||
get_binary_path,
|
get_binary_path,
|
||||||
|
get_cache_dir,
|
||||||
get_download_url,
|
get_download_url,
|
||||||
|
get_effective_version,
|
||||||
get_local_binary_override,
|
get_local_binary_override,
|
||||||
get_platform_tag,
|
get_platform_tag,
|
||||||
)
|
)
|
||||||
@@ -29,6 +37,9 @@ logger = logging.getLogger("cloakbrowser")
|
|||||||
# Timeout for download (large binary, allow 10 min)
|
# Timeout for download (large binary, allow 10 min)
|
||||||
DOWNLOAD_TIMEOUT = 600.0
|
DOWNLOAD_TIMEOUT = 600.0
|
||||||
|
|
||||||
|
# Auto-update check interval (1 hour)
|
||||||
|
UPDATE_CHECK_INTERVAL = 3600
|
||||||
|
|
||||||
|
|
||||||
def ensure_binary() -> str:
|
def ensure_binary() -> str:
|
||||||
"""Ensure the stealth Chromium binary is available. Download if needed.
|
"""Ensure the stealth Chromium binary is available. Download if needed.
|
||||||
@@ -48,13 +59,27 @@ def ensure_binary() -> str:
|
|||||||
logger.info("Using local binary override: %s", local_override)
|
logger.info("Using local binary override: %s", local_override)
|
||||||
return str(path)
|
return str(path)
|
||||||
|
|
||||||
# Check if binary is already cached
|
# Fail fast if no binary available for this platform
|
||||||
binary_path = get_binary_path()
|
check_platform_available()
|
||||||
|
|
||||||
|
# Check for auto-updated version first, then fall back to hardcoded
|
||||||
|
effective = get_effective_version()
|
||||||
|
binary_path = get_binary_path(effective)
|
||||||
|
|
||||||
if binary_path.exists() and _is_executable(binary_path):
|
if binary_path.exists() and _is_executable(binary_path):
|
||||||
logger.debug("Binary found in cache: %s", binary_path)
|
logger.debug("Binary found in cache: %s (version %s)", binary_path, effective)
|
||||||
|
_maybe_trigger_update_check()
|
||||||
return str(binary_path)
|
return str(binary_path)
|
||||||
|
|
||||||
# Download
|
# Fall back to hardcoded version if effective version binary doesn't exist
|
||||||
|
if effective != CHROMIUM_VERSION:
|
||||||
|
fallback_path = get_binary_path()
|
||||||
|
if fallback_path.exists() and _is_executable(fallback_path):
|
||||||
|
logger.debug("Binary found in cache: %s", fallback_path)
|
||||||
|
_maybe_trigger_update_check()
|
||||||
|
return str(fallback_path)
|
||||||
|
|
||||||
|
# Download hardcoded version
|
||||||
logger.info(
|
logger.info(
|
||||||
"Stealth Chromium %s not found. Downloading for %s...",
|
"Stealth Chromium %s not found. Downloading for %s...",
|
||||||
CHROMIUM_VERSION,
|
CHROMIUM_VERSION,
|
||||||
@@ -62,6 +87,7 @@ def ensure_binary() -> str:
|
|||||||
)
|
)
|
||||||
_download_and_extract()
|
_download_and_extract()
|
||||||
|
|
||||||
|
binary_path = get_binary_path()
|
||||||
if not binary_path.exists():
|
if not binary_path.exists():
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
f"Download completed but binary not found at expected path: {binary_path}. "
|
f"Download completed but binary not found at expected path: {binary_path}. "
|
||||||
@@ -69,13 +95,15 @@ def ensure_binary() -> str:
|
|||||||
f"https://github.com/CloakHQ/cloakbrowser/issues"
|
f"https://github.com/CloakHQ/cloakbrowser/issues"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_maybe_trigger_update_check()
|
||||||
return str(binary_path)
|
return str(binary_path)
|
||||||
|
|
||||||
|
|
||||||
def _download_and_extract() -> None:
|
def _download_and_extract(version: str | None = None) -> None:
|
||||||
"""Download the binary archive and extract to cache directory."""
|
"""Download the binary archive and extract to cache directory."""
|
||||||
url = get_download_url()
|
url = get_download_url(version)
|
||||||
binary_dir = get_binary_dir()
|
binary_dir = get_binary_dir(version)
|
||||||
|
binary_path = get_binary_path(version)
|
||||||
|
|
||||||
# Create cache dir
|
# Create cache dir
|
||||||
binary_dir.parent.mkdir(parents=True, exist_ok=True)
|
binary_dir.parent.mkdir(parents=True, exist_ok=True)
|
||||||
@@ -86,7 +114,7 @@ def _download_and_extract() -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
_download_file(url, tmp_path)
|
_download_file(url, tmp_path)
|
||||||
_extract_archive(tmp_path, binary_dir)
|
_extract_archive(tmp_path, binary_dir, binary_path)
|
||||||
finally:
|
finally:
|
||||||
# Clean up temp file
|
# Clean up temp file
|
||||||
tmp_path.unlink(missing_ok=True)
|
tmp_path.unlink(missing_ok=True)
|
||||||
@@ -123,7 +151,9 @@ def _download_file(url: str, dest: Path) -> None:
|
|||||||
logger.info("Download complete: %d MB", dest.stat().st_size // (1024 * 1024))
|
logger.info("Download complete: %d MB", dest.stat().st_size // (1024 * 1024))
|
||||||
|
|
||||||
|
|
||||||
def _extract_archive(archive_path: Path, dest_dir: Path) -> None:
|
def _extract_archive(
|
||||||
|
archive_path: Path, dest_dir: Path, binary_path: Path | None = None
|
||||||
|
) -> None:
|
||||||
"""Extract tar.gz archive to destination directory."""
|
"""Extract tar.gz archive to destination directory."""
|
||||||
logger.info("Extracting to %s", dest_dir)
|
logger.info("Extracting to %s", dest_dir)
|
||||||
|
|
||||||
@@ -153,10 +183,10 @@ def _extract_archive(archive_path: Path, dest_dir: Path) -> None:
|
|||||||
_flatten_single_subdir(dest_dir)
|
_flatten_single_subdir(dest_dir)
|
||||||
|
|
||||||
# Make binary executable
|
# Make binary executable
|
||||||
binary_path = get_binary_path()
|
bp = binary_path or get_binary_path()
|
||||||
if binary_path.exists():
|
if bp.exists():
|
||||||
_make_executable(binary_path)
|
_make_executable(bp)
|
||||||
logger.info("Binary ready: %s", binary_path)
|
logger.info("Binary ready: %s", bp)
|
||||||
|
|
||||||
|
|
||||||
def _flatten_single_subdir(dest_dir: Path) -> None:
|
def _flatten_single_subdir(dest_dir: Path) -> None:
|
||||||
@@ -200,12 +230,132 @@ def clear_cache() -> None:
|
|||||||
|
|
||||||
def binary_info() -> dict:
|
def binary_info() -> dict:
|
||||||
"""Return info about the current binary installation."""
|
"""Return info about the current binary installation."""
|
||||||
binary_path = get_binary_path()
|
effective = get_effective_version()
|
||||||
|
binary_path = get_binary_path(effective)
|
||||||
return {
|
return {
|
||||||
"version": CHROMIUM_VERSION,
|
"version": effective,
|
||||||
|
"bundled_version": CHROMIUM_VERSION,
|
||||||
"platform": get_platform_tag(),
|
"platform": get_platform_tag(),
|
||||||
"binary_path": str(binary_path),
|
"binary_path": str(binary_path),
|
||||||
"installed": binary_path.exists(),
|
"installed": binary_path.exists(),
|
||||||
"cache_dir": str(get_binary_dir()),
|
"cache_dir": str(get_binary_dir(effective)),
|
||||||
"download_url": get_download_url(),
|
"download_url": get_download_url(effective),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Auto-update
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def check_for_update() -> str | None:
|
||||||
|
"""Manually check for a newer Chromium version. Returns new version or None.
|
||||||
|
|
||||||
|
This is the public API for triggering an update check. Unlike the
|
||||||
|
background check in ensure_binary(), this blocks until complete.
|
||||||
|
"""
|
||||||
|
latest = _get_latest_chromium_version()
|
||||||
|
if latest is None:
|
||||||
|
return None
|
||||||
|
if not _version_newer(latest, CHROMIUM_VERSION):
|
||||||
|
return None
|
||||||
|
|
||||||
|
binary_dir = get_binary_dir(latest)
|
||||||
|
if binary_dir.exists():
|
||||||
|
# Already downloaded
|
||||||
|
_write_version_marker(latest)
|
||||||
|
return latest
|
||||||
|
|
||||||
|
logger.info("Downloading Chromium %s...", latest)
|
||||||
|
_download_and_extract(version=latest)
|
||||||
|
_write_version_marker(latest)
|
||||||
|
return latest
|
||||||
|
|
||||||
|
|
||||||
|
def _should_check_for_update() -> bool:
|
||||||
|
"""Check if auto-update is enabled and rate limit hasn't been hit."""
|
||||||
|
if os.environ.get("CLOAKBROWSER_AUTO_UPDATE", "").lower() == "false":
|
||||||
|
return False
|
||||||
|
if get_local_binary_override():
|
||||||
|
return False
|
||||||
|
if os.environ.get("CLOAKBROWSER_DOWNLOAD_URL"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
check_file = get_cache_dir() / ".last_update_check"
|
||||||
|
if check_file.exists():
|
||||||
|
try:
|
||||||
|
last_check = float(check_file.read_text().strip())
|
||||||
|
if time.time() - last_check < UPDATE_CHECK_INTERVAL:
|
||||||
|
return False
|
||||||
|
except (ValueError, OSError):
|
||||||
|
pass
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def _get_latest_chromium_version() -> str | None:
|
||||||
|
"""Hit GitHub Releases API, return latest chromium-v* version string or None."""
|
||||||
|
try:
|
||||||
|
resp = httpx.get(
|
||||||
|
GITHUB_API_URL, params={"per_page": 10}, timeout=10.0
|
||||||
|
)
|
||||||
|
resp.raise_for_status()
|
||||||
|
for release in resp.json():
|
||||||
|
tag = release.get("tag_name", "")
|
||||||
|
if tag.startswith("chromium-v") and not release.get("draft"):
|
||||||
|
return tag.removeprefix("chromium-v")
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Auto-update check failed", exc_info=True)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _write_version_marker(version: str) -> None:
|
||||||
|
"""Write the latest version marker to cache dir."""
|
||||||
|
cache_dir = get_cache_dir()
|
||||||
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
marker = cache_dir / "latest_version"
|
||||||
|
# Write to temp file then rename for atomicity
|
||||||
|
tmp = marker.with_suffix(".tmp")
|
||||||
|
tmp.write_text(version)
|
||||||
|
tmp.rename(marker)
|
||||||
|
|
||||||
|
|
||||||
|
def _check_and_download_update() -> None:
|
||||||
|
"""Background task: check for newer binary, download if available."""
|
||||||
|
try:
|
||||||
|
# Record check timestamp first (rate limiting)
|
||||||
|
check_file = get_cache_dir() / ".last_update_check"
|
||||||
|
check_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
check_file.write_text(str(time.time()))
|
||||||
|
|
||||||
|
latest = _get_latest_chromium_version()
|
||||||
|
if latest is None:
|
||||||
|
return
|
||||||
|
if not _version_newer(latest, CHROMIUM_VERSION):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Already downloaded?
|
||||||
|
if get_binary_dir(latest).exists():
|
||||||
|
_write_version_marker(latest)
|
||||||
|
return
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Newer Chromium available: %s (current: %s). Downloading in background...",
|
||||||
|
latest,
|
||||||
|
CHROMIUM_VERSION,
|
||||||
|
)
|
||||||
|
_download_and_extract(version=latest)
|
||||||
|
_write_version_marker(latest)
|
||||||
|
logger.info(
|
||||||
|
"Background update complete: Chromium %s ready. Will use on next launch.",
|
||||||
|
latest,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logger.debug("Background update failed", exc_info=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _maybe_trigger_update_check() -> None:
|
||||||
|
"""Fire-and-forget update check in a daemon thread."""
|
||||||
|
if not _should_check_for_update():
|
||||||
|
return
|
||||||
|
t = threading.Thread(target=_check_and_download_update, daemon=True)
|
||||||
|
t.start()
|
||||||
|
|||||||
+13
-2
@@ -45,6 +45,8 @@ await browser.close();
|
|||||||
|
|
||||||
### Puppeteer
|
### Puppeteer
|
||||||
|
|
||||||
|
> **Note:** Playwright is recommended for sites with reCAPTCHA Enterprise. Puppeteer's CDP protocol leaks automation signals that reCAPTCHA Enterprise can detect. This is a known Puppeteer limitation, not specific to CloakBrowser.
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { launch } from 'cloakbrowser/puppeteer';
|
import { launch } from 'cloakbrowser/puppeteer';
|
||||||
|
|
||||||
@@ -85,7 +87,7 @@ const context = await launchContext({
|
|||||||
### Utilities
|
### Utilities
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
import { ensureBinary, clearCache, binaryInfo } from 'cloakbrowser';
|
import { ensureBinary, clearCache, binaryInfo, checkForUpdate } from 'cloakbrowser';
|
||||||
|
|
||||||
// Pre-download binary (e.g., during Docker build)
|
// Pre-download binary (e.g., during Docker build)
|
||||||
await ensureBinary();
|
await ensureBinary();
|
||||||
@@ -95,6 +97,10 @@ console.log(binaryInfo());
|
|||||||
|
|
||||||
// Force re-download
|
// Force re-download
|
||||||
clearCache();
|
clearCache();
|
||||||
|
|
||||||
|
// Manually check for newer Chromium version
|
||||||
|
const newVersion = await checkForUpdate();
|
||||||
|
if (newVersion) console.log(`Updated to ${newVersion}`);
|
||||||
```
|
```
|
||||||
|
|
||||||
## Test Results
|
## Test Results
|
||||||
@@ -115,6 +121,7 @@ clearCache();
|
|||||||
| `CLOAKBROWSER_BINARY_PATH` | — | Skip download, use a local Chromium binary |
|
| `CLOAKBROWSER_BINARY_PATH` | — | Skip download, use a local Chromium binary |
|
||||||
| `CLOAKBROWSER_CACHE_DIR` | `~/.cloakbrowser` | Binary cache directory |
|
| `CLOAKBROWSER_CACHE_DIR` | `~/.cloakbrowser` | Binary cache directory |
|
||||||
| `CLOAKBROWSER_DOWNLOAD_URL` | GitHub Releases | Custom download URL |
|
| `CLOAKBROWSER_DOWNLOAD_URL` | GitHub Releases | Custom download URL |
|
||||||
|
| `CLOAKBROWSER_AUTO_UPDATE` | `true` | Set to `false` to disable background update checks |
|
||||||
|
|
||||||
## Migrate From Playwright
|
## Migrate From Playwright
|
||||||
|
|
||||||
@@ -130,13 +137,17 @@ const page = await browser.newPage();
|
|||||||
|
|
||||||
## Platforms
|
## Platforms
|
||||||
|
|
||||||
|
> **CloakBrowser is in active development.** Pre-built binaries are currently Linux-only. macOS and Windows builds are coming soon.
|
||||||
|
|
||||||
| Platform | Status |
|
| Platform | Status |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Linux x86_64 | ✅ Supported |
|
| Linux x86_64 | ✅ Available |
|
||||||
| macOS arm64 (Apple Silicon) | Coming soon |
|
| macOS arm64 (Apple Silicon) | Coming soon |
|
||||||
| macOS x86_64 (Intel) | Coming soon |
|
| macOS x86_64 (Intel) | Coming soon |
|
||||||
| Windows | Planned |
|
| Windows | Planned |
|
||||||
|
|
||||||
|
**On macOS/Windows?** You can still use CloakBrowser via Docker or with your own Chromium binary by setting `CLOAKBROWSER_BINARY_PATH=/path/to/chrome`.
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- Node.js >= 18
|
- Node.js >= 18
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloakbrowser",
|
"name": "cloakbrowser",
|
||||||
"version": "0.1.3",
|
"version": "0.1.5",
|
||||||
"description": "Stealth Chromium that passes every bot detection test. Drop-in Playwright/Puppeteer replacement with source-level fingerprint patches.",
|
"description": "Stealth Chromium that passes every bot detection test. Drop-in Playwright/Puppeteer replacement with source-level fingerprint patches.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
|
|||||||
+64
-8
@@ -3,6 +3,7 @@
|
|||||||
* Mirrors Python cloakbrowser/config.py.
|
* Mirrors Python cloakbrowser/config.py.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
@@ -21,6 +22,10 @@ const SUPPORTED_PLATFORMS: Record<string, string> = {
|
|||||||
"darwin-x64": "darwin-x64",
|
"darwin-x64": "darwin-x64",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Platforms with pre-built binaries available for download.
|
||||||
|
// Update this set as new platform builds are released.
|
||||||
|
const AVAILABLE_PLATFORMS = new Set(["linux-x64"]);
|
||||||
|
|
||||||
export function getPlatformTag(): string {
|
export function getPlatformTag(): string {
|
||||||
const platform = process.platform;
|
const platform = process.platform;
|
||||||
const arch = process.arch;
|
const arch = process.arch;
|
||||||
@@ -50,28 +55,79 @@ export function getCacheDir(): string {
|
|||||||
return path.join(os.homedir(), ".cloakbrowser");
|
return path.join(os.homedir(), ".cloakbrowser");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBinaryDir(): string {
|
export function getBinaryDir(version?: string): string {
|
||||||
return path.join(getCacheDir(), `chromium-${CHROMIUM_VERSION}`);
|
return path.join(getCacheDir(), `chromium-${version || CHROMIUM_VERSION}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getBinaryPath(): string {
|
export function getBinaryPath(version?: string): string {
|
||||||
const binaryDir = getBinaryDir();
|
const binaryDir = getBinaryDir(version);
|
||||||
if (process.platform === "darwin") {
|
if (process.platform === "darwin") {
|
||||||
return path.join(binaryDir, "Chromium.app", "Contents", "MacOS", "Chromium");
|
return path.join(binaryDir, "Chromium.app", "Contents", "MacOS", "Chromium");
|
||||||
}
|
}
|
||||||
return path.join(binaryDir, "chrome");
|
return path.join(binaryDir, "chrome");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkPlatformAvailable(): void {
|
||||||
|
if (getLocalBinaryOverride()) return;
|
||||||
|
|
||||||
|
const tag = getPlatformTag(); // throws if unsupported entirely
|
||||||
|
if (!AVAILABLE_PLATFORMS.has(tag)) {
|
||||||
|
const available = [...AVAILABLE_PLATFORMS].sort().join(", ");
|
||||||
|
throw new Error(
|
||||||
|
`CloakBrowser is in active development. ` +
|
||||||
|
`Pre-built binaries are currently only available for: ${available}.\n` +
|
||||||
|
`macOS and Windows builds are coming soon.\n\n` +
|
||||||
|
`To use CloakBrowser now, run in Docker (see README).`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Download URL
|
// Download URL
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const DOWNLOAD_BASE_URL =
|
export const DOWNLOAD_BASE_URL =
|
||||||
process.env.CLOAKBROWSER_DOWNLOAD_URL ||
|
process.env.CLOAKBROWSER_DOWNLOAD_URL ||
|
||||||
"https://github.com/CloakHQ/chromium-stealth-builds/releases/download";
|
"https://github.com/CloakHQ/cloakbrowser/releases/download";
|
||||||
|
|
||||||
export function getDownloadUrl(): string {
|
export const GITHUB_API_URL =
|
||||||
|
"https://api.github.com/repos/CloakHQ/cloakbrowser/releases";
|
||||||
|
|
||||||
|
export function getDownloadUrl(version?: string): string {
|
||||||
|
const v = version || CHROMIUM_VERSION;
|
||||||
const tag = getPlatformTag();
|
const tag = getPlatformTag();
|
||||||
return `${DOWNLOAD_BASE_URL}/v${CHROMIUM_VERSION}/cloakbrowser-${tag}.tar.gz`;
|
return `${DOWNLOAD_BASE_URL}/chromium-v${v}/cloakbrowser-${tag}.tar.gz`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getEffectiveVersion(): string {
|
||||||
|
const marker = path.join(getCacheDir(), "latest_version");
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(marker)) {
|
||||||
|
const version = fs.readFileSync(marker, "utf-8").trim();
|
||||||
|
if (version && versionNewer(version, CHROMIUM_VERSION)) {
|
||||||
|
const binary = getBinaryPath(version);
|
||||||
|
if (fs.existsSync(binary)) {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Marker unreadable — fall back to hardcoded
|
||||||
|
}
|
||||||
|
return CHROMIUM_VERSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseVersion(v: string): number[] {
|
||||||
|
return v.split(".").map(Number);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function versionNewer(a: string, b: string): boolean {
|
||||||
|
const va = parseVersion(a);
|
||||||
|
const vb = parseVersion(b);
|
||||||
|
for (let i = 0; i < Math.max(va.length, vb.length); i++) {
|
||||||
|
if ((va[i] ?? 0) > (vb[i] ?? 0)) return true;
|
||||||
|
if ((va[i] ?? 0) < (vb[i] ?? 0)) return false;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+153
-20
@@ -13,15 +13,20 @@ import { extract as tarExtract } from "tar";
|
|||||||
import type { BinaryInfo } from "./types.js";
|
import type { BinaryInfo } from "./types.js";
|
||||||
import {
|
import {
|
||||||
CHROMIUM_VERSION,
|
CHROMIUM_VERSION,
|
||||||
|
GITHUB_API_URL,
|
||||||
|
checkPlatformAvailable,
|
||||||
getBinaryDir,
|
getBinaryDir,
|
||||||
getBinaryPath,
|
getBinaryPath,
|
||||||
|
getCacheDir,
|
||||||
getDownloadUrl,
|
getDownloadUrl,
|
||||||
|
getEffectiveVersion,
|
||||||
getLocalBinaryOverride,
|
getLocalBinaryOverride,
|
||||||
getPlatformTag,
|
getPlatformTag,
|
||||||
getCacheDir,
|
versionNewer,
|
||||||
} from "./config.js";
|
} from "./config.js";
|
||||||
|
|
||||||
const DOWNLOAD_TIMEOUT_MS = 600_000; // 10 minutes
|
const DOWNLOAD_TIMEOUT_MS = 600_000; // 10 minutes
|
||||||
|
const UPDATE_CHECK_INTERVAL_MS = 3_600_000; // 1 hour
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Public API
|
// Public API
|
||||||
@@ -44,27 +49,44 @@ export async function ensureBinary(): Promise<string> {
|
|||||||
return localOverride;
|
return localOverride;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if binary is cached
|
// Fail fast if no binary available for this platform
|
||||||
const binaryPath = getBinaryPath();
|
checkPlatformAvailable();
|
||||||
|
|
||||||
|
// Check for auto-updated version first, then fall back to hardcoded
|
||||||
|
const effective = getEffectiveVersion();
|
||||||
|
const binaryPath = getBinaryPath(effective);
|
||||||
|
|
||||||
if (fs.existsSync(binaryPath) && isExecutable(binaryPath)) {
|
if (fs.existsSync(binaryPath) && isExecutable(binaryPath)) {
|
||||||
|
maybeTriggerUpdateCheck();
|
||||||
return binaryPath;
|
return binaryPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Download
|
// Fall back to hardcoded version if effective version binary doesn't exist
|
||||||
|
if (effective !== CHROMIUM_VERSION) {
|
||||||
|
const fallbackPath = getBinaryPath();
|
||||||
|
if (fs.existsSync(fallbackPath) && isExecutable(fallbackPath)) {
|
||||||
|
maybeTriggerUpdateCheck();
|
||||||
|
return fallbackPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download hardcoded version
|
||||||
console.log(
|
console.log(
|
||||||
`[cloakbrowser] Stealth Chromium ${CHROMIUM_VERSION} not found. Downloading for ${getPlatformTag()}...`
|
`[cloakbrowser] Stealth Chromium ${CHROMIUM_VERSION} not found. Downloading for ${getPlatformTag()}...`
|
||||||
);
|
);
|
||||||
await downloadAndExtract();
|
await downloadAndExtract();
|
||||||
|
|
||||||
if (!fs.existsSync(binaryPath)) {
|
const downloadedPath = getBinaryPath();
|
||||||
|
if (!fs.existsSync(downloadedPath)) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Download completed but binary not found at expected path: ${binaryPath}. ` +
|
`Download completed but binary not found at expected path: ${downloadedPath}. ` +
|
||||||
`This may indicate a packaging issue. Please report at ` +
|
`This may indicate a packaging issue. Please report at ` +
|
||||||
`https://github.com/CloakHQ/cloakbrowser/issues`
|
`https://github.com/CloakHQ/cloakbrowser/issues`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return binaryPath;
|
maybeTriggerUpdateCheck();
|
||||||
|
return downloadedPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove all cached binaries. Forces re-download on next launch. */
|
/** Remove all cached binaries. Forces re-download on next launch. */
|
||||||
@@ -78,24 +100,43 @@ export function clearCache(): void {
|
|||||||
|
|
||||||
/** Return info about the current binary installation. */
|
/** Return info about the current binary installation. */
|
||||||
export function binaryInfo(): BinaryInfo {
|
export function binaryInfo(): BinaryInfo {
|
||||||
const binaryPath = getBinaryPath();
|
const effective = getEffectiveVersion();
|
||||||
|
const binaryPath = getBinaryPath(effective);
|
||||||
return {
|
return {
|
||||||
version: CHROMIUM_VERSION,
|
version: effective,
|
||||||
platform: getPlatformTag(),
|
platform: getPlatformTag(),
|
||||||
binaryPath,
|
binaryPath,
|
||||||
installed: fs.existsSync(binaryPath),
|
installed: fs.existsSync(binaryPath),
|
||||||
cacheDir: getBinaryDir(),
|
cacheDir: getBinaryDir(effective),
|
||||||
downloadUrl: getDownloadUrl(),
|
downloadUrl: getDownloadUrl(effective),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Manually check for a newer Chromium version. Returns new version or null. */
|
||||||
|
export async function checkForUpdate(): Promise<string | null> {
|
||||||
|
const latest = await getLatestChromiumVersion();
|
||||||
|
if (!latest || !versionNewer(latest, CHROMIUM_VERSION)) return null;
|
||||||
|
|
||||||
|
const binaryDir = getBinaryDir(latest);
|
||||||
|
if (fs.existsSync(binaryDir)) {
|
||||||
|
writeVersionMarker(latest);
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[cloakbrowser] Downloading Chromium ${latest}...`);
|
||||||
|
await downloadAndExtract(latest);
|
||||||
|
writeVersionMarker(latest);
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Internal helpers
|
// Internal helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async function downloadAndExtract(): Promise<void> {
|
async function downloadAndExtract(version?: string): Promise<void> {
|
||||||
const url = getDownloadUrl();
|
const url = getDownloadUrl(version);
|
||||||
const binaryDir = getBinaryDir();
|
const binaryDir = getBinaryDir(version);
|
||||||
|
const binaryPath = getBinaryPath(version);
|
||||||
|
|
||||||
// Create cache dir
|
// Create cache dir
|
||||||
fs.mkdirSync(path.dirname(binaryDir), { recursive: true });
|
fs.mkdirSync(path.dirname(binaryDir), { recursive: true });
|
||||||
@@ -108,7 +149,7 @@ async function downloadAndExtract(): Promise<void> {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await downloadFile(url, tmpPath);
|
await downloadFile(url, tmpPath);
|
||||||
await extractArchive(tmpPath, binaryDir);
|
await extractArchive(tmpPath, binaryDir, binaryPath);
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up temp file
|
// Clean up temp file
|
||||||
if (fs.existsSync(tmpPath)) {
|
if (fs.existsSync(tmpPath)) {
|
||||||
@@ -180,7 +221,8 @@ async function downloadFile(url: string, dest: string): Promise<void> {
|
|||||||
|
|
||||||
async function extractArchive(
|
async function extractArchive(
|
||||||
archivePath: string,
|
archivePath: string,
|
||||||
destDir: string
|
destDir: string,
|
||||||
|
binaryPath?: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
console.log(`[cloakbrowser] Extracting to ${destDir}`);
|
console.log(`[cloakbrowser] Extracting to ${destDir}`);
|
||||||
|
|
||||||
@@ -212,10 +254,10 @@ async function extractArchive(
|
|||||||
flattenSingleSubdir(destDir);
|
flattenSingleSubdir(destDir);
|
||||||
|
|
||||||
// Make binary executable
|
// Make binary executable
|
||||||
const binaryPath = getBinaryPath();
|
const bp = binaryPath || getBinaryPath();
|
||||||
if (fs.existsSync(binaryPath)) {
|
if (fs.existsSync(bp)) {
|
||||||
fs.chmodSync(binaryPath, 0o755);
|
fs.chmodSync(bp, 0o755);
|
||||||
console.log(`[cloakbrowser] Binary ready: ${binaryPath}`);
|
console.log(`[cloakbrowser] Binary ready: ${bp}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,3 +290,94 @@ function isExecutable(filePath: string): boolean {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Auto-update
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function shouldCheckForUpdate(): boolean {
|
||||||
|
if (process.env.CLOAKBROWSER_AUTO_UPDATE?.toLowerCase() === "false")
|
||||||
|
return false;
|
||||||
|
if (getLocalBinaryOverride()) return false;
|
||||||
|
if (process.env.CLOAKBROWSER_DOWNLOAD_URL) return false;
|
||||||
|
|
||||||
|
const checkFile = path.join(getCacheDir(), ".last_update_check");
|
||||||
|
try {
|
||||||
|
const lastCheck = Number(fs.readFileSync(checkFile, "utf-8").trim());
|
||||||
|
if (Date.now() - lastCheck < UPDATE_CHECK_INTERVAL_MS) return false;
|
||||||
|
} catch {
|
||||||
|
/* file doesn't exist or unreadable */
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLatestChromiumVersion(): Promise<string | null> {
|
||||||
|
try {
|
||||||
|
const resp = await fetch(`${GITHUB_API_URL}?per_page=10`, {
|
||||||
|
signal: AbortSignal.timeout(10_000),
|
||||||
|
});
|
||||||
|
if (!resp.ok) return null;
|
||||||
|
const releases = (await resp.json()) as Array<{
|
||||||
|
tag_name: string;
|
||||||
|
draft: boolean;
|
||||||
|
}>;
|
||||||
|
for (const release of releases) {
|
||||||
|
if (release.tag_name.startsWith("chromium-v") && !release.draft) {
|
||||||
|
return release.tag_name.replace("chromium-v", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeVersionMarker(version: string): void {
|
||||||
|
const cacheDir = getCacheDir();
|
||||||
|
fs.mkdirSync(cacheDir, { recursive: true });
|
||||||
|
const marker = path.join(cacheDir, "latest_version");
|
||||||
|
const tmp = `${marker}.tmp`;
|
||||||
|
fs.writeFileSync(tmp, version);
|
||||||
|
fs.renameSync(tmp, marker);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAndDownloadUpdate(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// Record check timestamp first (rate limiting)
|
||||||
|
const cacheDir = getCacheDir();
|
||||||
|
fs.mkdirSync(cacheDir, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(cacheDir, ".last_update_check"),
|
||||||
|
String(Date.now())
|
||||||
|
);
|
||||||
|
|
||||||
|
const latest = await getLatestChromiumVersion();
|
||||||
|
if (!latest || !versionNewer(latest, CHROMIUM_VERSION)) return;
|
||||||
|
|
||||||
|
// Already downloaded?
|
||||||
|
if (fs.existsSync(getBinaryDir(latest))) {
|
||||||
|
writeVersionMarker(latest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`[cloakbrowser] Newer Chromium available: ${latest} (current: ${CHROMIUM_VERSION}). Downloading in background...`
|
||||||
|
);
|
||||||
|
await downloadAndExtract(latest);
|
||||||
|
writeVersionMarker(latest);
|
||||||
|
console.log(
|
||||||
|
`[cloakbrowser] Background update complete: Chromium ${latest} ready. Will use on next launch.`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
// Background update failed — don't disrupt the user
|
||||||
|
if (process.env.DEBUG) {
|
||||||
|
console.error("[cloakbrowser] Background update failed:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeTriggerUpdateCheck(): void {
|
||||||
|
if (!shouldCheckForUpdate()) return;
|
||||||
|
// Fire-and-forget — don't await
|
||||||
|
checkAndDownloadUpdate().catch(() => {});
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -19,7 +19,7 @@
|
|||||||
export { launch, launchContext } from "./playwright.js";
|
export { launch, launchContext } from "./playwright.js";
|
||||||
|
|
||||||
// Binary management
|
// Binary management
|
||||||
export { ensureBinary, clearCache, binaryInfo } from "./download.js";
|
export { ensureBinary, clearCache, binaryInfo, checkForUpdate } from "./download.js";
|
||||||
|
|
||||||
// Config
|
// Config
|
||||||
export { CHROMIUM_VERSION, getDefaultStealthArgs } from "./config.js";
|
export { CHROMIUM_VERSION, getDefaultStealthArgs } from "./config.js";
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { describe, it, expect } from "vitest";
|
||||||
|
import {
|
||||||
|
CHROMIUM_VERSION,
|
||||||
|
getDownloadUrl,
|
||||||
|
getEffectiveVersion,
|
||||||
|
parseVersion,
|
||||||
|
versionNewer,
|
||||||
|
} from "../src/config.js";
|
||||||
|
|
||||||
|
describe("version comparison", () => {
|
||||||
|
it("parseVersion handles 4-part versions", () => {
|
||||||
|
expect(parseVersion("145.0.7718.0")).toEqual([145, 0, 7718, 0]);
|
||||||
|
expect(parseVersion("142.0.7444.175")).toEqual([142, 0, 7444, 175]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects newer version", () => {
|
||||||
|
expect(versionNewer("145.0.7718.0", "142.0.7444.175")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("detects older version", () => {
|
||||||
|
expect(versionNewer("142.0.7444.175", "145.0.7718.0")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("same version is not newer", () => {
|
||||||
|
expect(versionNewer("142.0.7444.175", "142.0.7444.175")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("patch bump detected", () => {
|
||||||
|
expect(versionNewer("142.0.7444.176", "142.0.7444.175")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("major bump wins over minor", () => {
|
||||||
|
expect(versionNewer("143.0.0.0", "142.9.9999.999")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("download URL", () => {
|
||||||
|
it("uses chromium-v prefix and cloakbrowser repo", () => {
|
||||||
|
const url = getDownloadUrl();
|
||||||
|
expect(url).toContain("github.com/CloakHQ/cloakbrowser/releases/download");
|
||||||
|
expect(url).toContain(`chromium-v${CHROMIUM_VERSION}`);
|
||||||
|
expect(url.endsWith(".tar.gz")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("accepts custom version", () => {
|
||||||
|
const url = getDownloadUrl("145.0.7718.0");
|
||||||
|
expect(url).toContain("chromium-v145.0.7718.0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not reference old repo", () => {
|
||||||
|
const url = getDownloadUrl();
|
||||||
|
expect(url).not.toContain("chromium-stealth-builds");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("effective version", () => {
|
||||||
|
it("returns CHROMIUM_VERSION when no marker exists", () => {
|
||||||
|
// Default behavior — no marker file in test environment
|
||||||
|
expect(getEffectiveVersion()).toBe(CHROMIUM_VERSION);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
"""Tests for auto-update and version management."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cloakbrowser.config import (
|
||||||
|
CHROMIUM_VERSION,
|
||||||
|
_version_newer,
|
||||||
|
_version_tuple,
|
||||||
|
get_download_url,
|
||||||
|
get_effective_version,
|
||||||
|
)
|
||||||
|
from cloakbrowser.download import (
|
||||||
|
_get_latest_chromium_version,
|
||||||
|
_should_check_for_update,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestVersionComparison:
|
||||||
|
def test_version_tuple_parsing(self):
|
||||||
|
assert _version_tuple("145.0.7718.0") == (145, 0, 7718, 0)
|
||||||
|
assert _version_tuple("142.0.7444.175") == (142, 0, 7444, 175)
|
||||||
|
|
||||||
|
def test_newer_version(self):
|
||||||
|
assert _version_newer("145.0.7718.0", "142.0.7444.175") is True
|
||||||
|
|
||||||
|
def test_older_version(self):
|
||||||
|
assert _version_newer("142.0.7444.175", "145.0.7718.0") is False
|
||||||
|
|
||||||
|
def test_same_version(self):
|
||||||
|
assert _version_newer("142.0.7444.175", "142.0.7444.175") is False
|
||||||
|
|
||||||
|
def test_patch_bump(self):
|
||||||
|
assert _version_newer("142.0.7444.176", "142.0.7444.175") is True
|
||||||
|
|
||||||
|
def test_major_bump(self):
|
||||||
|
assert _version_newer("143.0.0.0", "142.9.9999.999") is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestDownloadUrl:
|
||||||
|
def test_default_url_format(self):
|
||||||
|
url = get_download_url()
|
||||||
|
assert "github.com/CloakHQ/cloakbrowser/releases/download" in url
|
||||||
|
assert f"chromium-v{CHROMIUM_VERSION}" in url
|
||||||
|
assert url.endswith(".tar.gz")
|
||||||
|
|
||||||
|
def test_custom_version_url(self):
|
||||||
|
url = get_download_url("145.0.7718.0")
|
||||||
|
assert "chromium-v145.0.7718.0" in url
|
||||||
|
|
||||||
|
def test_no_old_repo_reference(self):
|
||||||
|
url = get_download_url()
|
||||||
|
assert "chromium-stealth-builds" not in url
|
||||||
|
|
||||||
|
|
||||||
|
class TestShouldCheckForUpdate:
|
||||||
|
def test_disabled_by_env(self):
|
||||||
|
with patch.dict(os.environ, {"CLOAKBROWSER_AUTO_UPDATE": "false"}):
|
||||||
|
assert _should_check_for_update() is False
|
||||||
|
|
||||||
|
def test_disabled_by_env_case_insensitive(self):
|
||||||
|
with patch.dict(os.environ, {"CLOAKBROWSER_AUTO_UPDATE": "False"}):
|
||||||
|
assert _should_check_for_update() is False
|
||||||
|
|
||||||
|
def test_disabled_by_binary_override(self):
|
||||||
|
with patch.dict(os.environ, {"CLOAKBROWSER_BINARY_PATH": "/some/path"}):
|
||||||
|
assert _should_check_for_update() is False
|
||||||
|
|
||||||
|
def test_disabled_by_custom_download_url(self):
|
||||||
|
with patch.dict(
|
||||||
|
os.environ, {"CLOAKBROWSER_DOWNLOAD_URL": "https://my-mirror.com"}
|
||||||
|
):
|
||||||
|
assert _should_check_for_update() is False
|
||||||
|
|
||||||
|
def test_rate_limited(self, tmp_path):
|
||||||
|
import time
|
||||||
|
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"CLOAKBROWSER_CACHE_DIR": str(tmp_path),
|
||||||
|
"CLOAKBROWSER_BINARY_PATH": "",
|
||||||
|
"CLOAKBROWSER_AUTO_UPDATE": "",
|
||||||
|
"CLOAKBROWSER_DOWNLOAD_URL": "",
|
||||||
|
},
|
||||||
|
):
|
||||||
|
check_file = tmp_path / ".last_update_check"
|
||||||
|
check_file.write_text(str(time.time()))
|
||||||
|
assert _should_check_for_update() is False
|
||||||
|
|
||||||
|
def test_stale_rate_limit_allows_check(self, tmp_path):
|
||||||
|
import time
|
||||||
|
|
||||||
|
with patch.dict(
|
||||||
|
os.environ,
|
||||||
|
{
|
||||||
|
"CLOAKBROWSER_CACHE_DIR": str(tmp_path),
|
||||||
|
"CLOAKBROWSER_BINARY_PATH": "",
|
||||||
|
"CLOAKBROWSER_AUTO_UPDATE": "",
|
||||||
|
"CLOAKBROWSER_DOWNLOAD_URL": "",
|
||||||
|
},
|
||||||
|
):
|
||||||
|
check_file = tmp_path / ".last_update_check"
|
||||||
|
check_file.write_text(str(time.time() - 7200)) # 2 hours ago
|
||||||
|
assert _should_check_for_update() is True
|
||||||
|
|
||||||
|
|
||||||
|
class TestEffectiveVersion:
|
||||||
|
def test_no_marker_returns_hardcoded(self, tmp_path):
|
||||||
|
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}):
|
||||||
|
assert get_effective_version() == CHROMIUM_VERSION
|
||||||
|
|
||||||
|
def test_marker_with_newer_version(self, tmp_path):
|
||||||
|
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}):
|
||||||
|
marker = tmp_path / "latest_version"
|
||||||
|
marker.write_text("999.0.0.0")
|
||||||
|
# Binary doesn't exist, so should fall back
|
||||||
|
assert get_effective_version() == CHROMIUM_VERSION
|
||||||
|
|
||||||
|
def test_marker_with_older_version_ignored(self, tmp_path):
|
||||||
|
with patch.dict(os.environ, {"CLOAKBROWSER_CACHE_DIR": str(tmp_path)}):
|
||||||
|
marker = tmp_path / "latest_version"
|
||||||
|
marker.write_text("100.0.0.0")
|
||||||
|
assert get_effective_version() == CHROMIUM_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
class TestGetLatestVersion:
|
||||||
|
def test_parses_chromium_tag(self):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = [
|
||||||
|
{"tag_name": "chromium-v145.0.7718.0", "draft": False},
|
||||||
|
{"tag_name": "chromium-v142.0.7444.175", "draft": False},
|
||||||
|
]
|
||||||
|
mock_response.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch("cloakbrowser.download.httpx.get", return_value=mock_response):
|
||||||
|
result = _get_latest_chromium_version()
|
||||||
|
assert result == "145.0.7718.0"
|
||||||
|
|
||||||
|
def test_skips_draft_releases(self):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = [
|
||||||
|
{"tag_name": "chromium-v999.0.0.0", "draft": True},
|
||||||
|
{"tag_name": "chromium-v145.0.7718.0", "draft": False},
|
||||||
|
]
|
||||||
|
mock_response.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch("cloakbrowser.download.httpx.get", return_value=mock_response):
|
||||||
|
result = _get_latest_chromium_version()
|
||||||
|
assert result == "145.0.7718.0"
|
||||||
|
|
||||||
|
def test_skips_non_chromium_tags(self):
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = [
|
||||||
|
{"tag_name": "v0.2.0", "draft": False},
|
||||||
|
{"tag_name": "chromium-v145.0.7718.0", "draft": False},
|
||||||
|
]
|
||||||
|
mock_response.raise_for_status = MagicMock()
|
||||||
|
|
||||||
|
with patch("cloakbrowser.download.httpx.get", return_value=mock_response):
|
||||||
|
result = _get_latest_chromium_version()
|
||||||
|
assert result == "145.0.7718.0"
|
||||||
|
|
||||||
|
def test_network_error_returns_none(self):
|
||||||
|
with patch("cloakbrowser.download.httpx.get", side_effect=Exception("timeout")):
|
||||||
|
result = _get_latest_chromium_version()
|
||||||
|
assert result is None
|
||||||
Reference in New Issue
Block a user