diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c2d4e..7c29343 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,11 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi Chromium v145 upgrade. 26 fingerprint patches (up from 16). New download verification and fallback system. Pending: macOS v145 binary builds. +### Breaking + +- **[wrapper]** Python dependency changed from `playwright` to `patchright` (CDP stealth fork). Patchright is API-compatible, but if you import `playwright` directly elsewhere, add it as a separate dependency. Replace `from playwright.sync_api` with `from patchright.sync_api` (or keep using `cloakbrowser.launch()` which handles this automatically). +- **[wrapper]** `launch_context()` / `launchContext()` now defaults viewport to 1920x955 (realistic maximized Chrome on 1080p Windows) instead of Playwright's default 1280x720. Pass `viewport={"width": 1280, "height": 720}` explicitly to restore old behavior. + ### 2026-03-01 - **[wrapper]** Upgrade wrapper to Chromium v145.0.7632.109 diff --git a/README.md b/README.md index 8acf36d..b5175fb 100644 --- a/README.md +++ b/README.md @@ -324,6 +324,7 @@ clearCache(); | `CLOAKBROWSER_CACHE_DIR` | `~/.cloakbrowser` | Binary cache directory | | `CLOAKBROWSER_DOWNLOAD_URL` | `cloakbrowser.dev` | Custom download URL for binary | | `CLOAKBROWSER_AUTO_UPDATE` | `true` | Set to `false` to disable background update checks | +| `CLOAKBROWSER_SKIP_CHECKSUM` | `false` | Set to `true` to skip SHA-256 verification after download | ## Fingerprint Management diff --git a/cloakbrowser/config.py b/cloakbrowser/config.py index 39d0117..3f5ee4b 100644 --- a/cloakbrowser/config.py +++ b/cloakbrowser/config.py @@ -162,17 +162,19 @@ def get_effective_version() -> str: Returns the platform's hardcoded version if no update has been downloaded. """ base = get_chromium_version() - marker = get_cache_dir() / f"latest_version_{get_platform_tag()}" - if marker.exists(): - try: - version = marker.read_text().strip() - if version and _version_newer(version, base): - # Verify the binary actually exists - binary = get_binary_path(version) - if binary.exists(): - return version - except (ValueError, OSError): - pass + # 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 diff --git a/examples/fingerprint_scan_test.py b/examples/fingerprint_scan_test.py index e390fc5..53bf7e7 100644 --- a/examples/fingerprint_scan_test.py +++ b/examples/fingerprint_scan_test.py @@ -13,6 +13,7 @@ Usage: """ import sys +import time from cloakbrowser import launch_context @@ -27,7 +28,7 @@ def test_fingerprint_scan(page): """fingerprint-scan.com — bot risk score + headless detection signals.""" print("=== fingerprint-scan.com ===") page.goto("https://fingerprint-scan.com/", wait_until="domcontentloaded", timeout=30000) - page.wait_for_timeout(20000) # Castle.js needs time to compute score + time.sleep(20) # Castle.js needs time to compute score # Check bot risk score score = page.evaluate( @@ -92,7 +93,7 @@ def test_creepjs(page): "https://abrahamjuliot.github.io/creepjs/", wait_until="domcontentloaded", timeout=30000 ) print("Waiting 30s for CreepJS analysis...") - page.wait_for_timeout(30000) + time.sleep(30) # Extract % scores from page text (matches test-infra/matrix_tests/group3_bot_detection.py) scores = page.evaluate("""() => { diff --git a/js/src/config.ts b/js/src/config.ts index e638d76..c338846 100644 --- a/js/src/config.ts +++ b/js/src/config.ts @@ -122,19 +122,23 @@ export function getFallbackDownloadUrl(version?: string): string { export function getEffectiveVersion(): string { const base = getChromiumVersion(); - const marker = path.join(getCacheDir(), `latest_version_${getPlatformTag()}`); - try { - if (fs.existsSync(marker)) { - const version = fs.readFileSync(marker, "utf-8").trim(); - if (version && versionNewer(version, base)) { - const binary = getBinaryPath(version); - if (fs.existsSync(binary)) { - return version; + const cacheDir = getCacheDir(); + // Try platform-scoped marker first, fall back to legacy marker for upgrades from <0.3.0 + for (const name of [`latest_version_${getPlatformTag()}`, "latest_version"]) { + const marker = path.join(cacheDir, name); + try { + if (fs.existsSync(marker)) { + const version = fs.readFileSync(marker, "utf-8").trim(); + if (version && versionNewer(version, base)) { + const binary = getBinaryPath(version); + if (fs.existsSync(binary)) { + return version; + } } } + } catch { + // Marker unreadable — try next } - } catch { - // Marker unreadable — fall back to hardcoded } return base; } diff --git a/js/src/download.ts b/js/src/download.ts index bfff7f0..fe82cb6 100644 --- a/js/src/download.ts +++ b/js/src/download.ts @@ -234,12 +234,13 @@ async function fetchChecksums(version?: string): Promise | n return null; } -function parseChecksums(text: string): Map { +/** @internal Exported for testing only. */ +export function parseChecksums(text: string): Map { const result = new Map(); for (const line of text.trim().split("\n")) { const trimmed = line.trim(); if (!trimmed) continue; - const match = trimmed.match(/^([a-f0-9]{64})\s+\*?(.+)$/); + const match = trimmed.match(/^([a-f0-9]{64})\s+\*?(.+)$/i); if (match) { result.set(match[2]!, match[1]!.toLowerCase()); } diff --git a/js/tests/update.test.ts b/js/tests/update.test.ts index 6b7ec81..1f8be6a 100644 --- a/js/tests/update.test.ts +++ b/js/tests/update.test.ts @@ -8,7 +8,7 @@ import { parseVersion, versionNewer, } from "../src/config.js"; -import { getLatestChromiumVersion } from "../src/download.js"; +import { getLatestChromiumVersion, parseChecksums } from "../src/download.js"; describe("version comparison", () => { it("parseVersion handles 4-part versions", () => { @@ -149,6 +149,38 @@ describe("latest version (platform-aware)", () => { }); }); +describe("parseChecksums", () => { + // Valid 64-char hex strings for testing + const HASH_A = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"; + const HASH_B = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2"; + + it("parses standard SHA256SUMS format", () => { + const text = [ + `${HASH_A} cloakbrowser-linux-x64.tar.gz`, + `${HASH_B} cloakbrowser-darwin-arm64.tar.gz`, + ].join("\n"); + const result = parseChecksums(text); + expect(result.get("cloakbrowser-linux-x64.tar.gz")).toBe(HASH_A); + expect(result.get("cloakbrowser-darwin-arm64.tar.gz")).toBe(HASH_B); + }); + + it("handles binary-mode asterisk prefix", () => { + const text = `${HASH_A} *cloakbrowser-linux-x64.tar.gz`; + const result = parseChecksums(text); + expect(result.has("cloakbrowser-linux-x64.tar.gz")).toBe(true); + }); + + it("skips empty lines", () => { + const text = `\n\n${HASH_A} file.tar.gz\n\n`; + expect(parseChecksums(text).size).toBe(1); + }); + + it("returns empty map for empty input", () => { + expect(parseChecksums("").size).toBe(0); + expect(parseChecksums(" \n \n").size).toBe(0); + }); +}); + describe("effective version", () => { it("returns platform version when no marker exists", () => { // Default behavior — no marker file in test environment diff --git a/tests/test_update.py b/tests/test_update.py index 5f1c82e..16204d4 100644 --- a/tests/test_update.py +++ b/tests/test_update.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import os from pathlib import Path from unittest.mock import MagicMock, patch @@ -19,7 +20,9 @@ from cloakbrowser.config import ( ) from cloakbrowser.download import ( _get_latest_chromium_version, + _parse_checksums, _should_check_for_update, + _verify_checksum, ) @@ -242,3 +245,52 @@ class TestGetLatestVersion: with patch("cloakbrowser.download.httpx.get", side_effect=Exception("timeout")): result = _get_latest_chromium_version() assert result is None + + +class TestParseChecksums: + HASH_A = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + HASH_B = "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" + + def test_standard_format(self): + text = ( + f"{self.HASH_A} cloakbrowser-linux-x64.tar.gz\n" + f"{self.HASH_B} cloakbrowser-darwin-arm64.tar.gz\n" + ) + result = _parse_checksums(text) + assert result["cloakbrowser-linux-x64.tar.gz"] == self.HASH_A + assert result["cloakbrowser-darwin-arm64.tar.gz"] == self.HASH_B + + def test_binary_mode_asterisk(self): + text = f"{self.HASH_A} *cloakbrowser-linux-x64.tar.gz\n" + result = _parse_checksums(text) + assert "cloakbrowser-linux-x64.tar.gz" in result + + def test_empty_lines_skipped(self): + text = f"\n\n{self.HASH_A} file.tar.gz\n\n" + result = _parse_checksums(text) + assert len(result) == 1 + + def test_uppercase_lowered(self): + text = f"{self.HASH_A.upper()} file.tar.gz\n" + result = _parse_checksums(text) + assert result["file.tar.gz"] == self.HASH_A + + def test_empty_input(self): + assert _parse_checksums("") == {} + assert _parse_checksums(" \n \n") == {} + + +class TestVerifyChecksum: + def test_matching_checksum(self, tmp_path): + content = b"test binary content" + file = tmp_path / "test.tar.gz" + file.write_bytes(content) + expected = hashlib.sha256(content).hexdigest() + # Should not raise + _verify_checksum(file, expected) + + def test_mismatched_checksum(self, tmp_path): + file = tmp_path / "test.tar.gz" + file.write_bytes(b"real content") + with pytest.raises(RuntimeError, match="Checksum verification failed"): + _verify_checksum(file, "0" * 64)