Compare commits

...
Author SHA1 Message Date
CloakHQ d2a42fc86b release: v0.3.15 — upgrade Linux binary to .159.7, StorageBuckets normalization
Binary:
- Upgrade Linux x64 build to Chromium 145.0.7632.159.7 (33 C++ patches)
- StorageBuckets API quota normalization — closes last storage-based incognito detection vector

Wrapper:
- Fix non-ASCII character support in humanized typing (Cyrillic, CJK, emoji)
- Document storage quota tradeoff for persistent contexts
- Add Ko-fi funding link
2026-03-13 18:59:18 +01:00
lilos 1af25d67bc fix: support non-ASCII characters (Cyrillic, CJK, emoji) in humanized typing 2026-03-13 00:34:01 +01:00
CloakHQ 1bef989404 test: add download fallback tests for primary → GitHub failover
Verify that HTTP errors (429, 503, etc.) from cloakbrowser.dev
correctly trigger GitHub Releases fallback for both binary and
checksum downloads. Also test that custom CLOAKBROWSER_DOWNLOAD_URL
disables fallback, and both-sources-fail returns gracefully.
2026-03-12 19:47:49 +01:00
CloakHQ 0aa4ea56bd docs: add Browser Profile Manager section to README 2026-03-12 01:58:26 +01:00
CloakHQ c0ba21faa1 release: v0.3.14 — upgrade Linux binary to .159.6, add binary management CLI
Binary:
- Upgrade Linux build to 145.0.7632.159.6 (32 patches)
- Fix persistent context fingerprint consistency
- Storage quota normalization for persistent context profiles
- Fix window dimension calculation for non-incognito contexts

Wrapper:
- Add CLI for binary management with visible download progress (closes #43)
- Python: python -m cloakbrowser install|info|update|clear-cache
- JavaScript: npx cloakbrowser install|info|update|clear-cache
2026-03-11 23:44:41 +01:00
CloakHQ b501d8f158 chore: gitignore browser profile manager directory 2026-03-11 18:13:19 +01:00
CloakHQ 6007a6e511 feat: add CLI for binary management (Python + JavaScript)
Adds install, info, update, and clear-cache subcommands with visible
download progress. Python: `python -m cloakbrowser install`. JavaScript:
`npx cloakbrowser install`. Useful for Dockerfiles where silent
first-use downloads are hard to debug. Closes #43.
2026-03-11 04:33:37 +01:00
CloakHQ 96c55352e0 ci: remove deployment environments from publish workflow to hide actor identity 2026-03-11 00:22:57 +01:00
CloakHQ 5d35fb9e4c release: v0.3.13 — suppress SwiftShader default arg, upgrade Linux binary to .159.5 2026-03-10 23:11:12 +01:00
CloakHQ c966e046e7 docs: add Docker signature verification, deployment environments, improve troubleshooting
- Add cosign verify command to README Security section for Docker image verification
- Add GitHub deployment environments (pypi, npm, docker) to publish workflow for sidebar status tracking
- Simplify downgrade instructions: version-pinned pip/npm/docker instead of manual binary paths
- Improve troubleshooting section with headings and dividers for readability
- Update Latest section to v0.3.12 with new binary features
2026-03-10 07:27:18 +01:00
23 changed files with 743 additions and 54 deletions
+1
View File
@@ -0,0 +1 @@
ko_fi: cloakhq
+3
View File
@@ -56,6 +56,9 @@ test-infra/
# Website (deployed separately) # Website (deployed separately)
site/ site/
# Browser profile manager (deployed separately)
manager/
# Release scripts # Release scripts
publish.sh publish.sh
deploy.sh deploy.sh
+18
View File
@@ -6,6 +6,24 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi
--- ---
## [0.3.15] — 2026-03-13
- **[binary]** Upgrade Linux build to 145.0.7632.159.7 — 33 source-level C++ patches
- **[binary]** StorageBuckets API quota normalization — closes the last storage-based incognito detection vector
- **[wrapper]** Fix non-ASCII character support in humanized typing — Cyrillic, CJK, and emoji now type correctly (thanks [@evelaa123](https://github.com/evelaa123))
## [0.3.14] — 2026-03-12
- **[binary]** Upgrade Linux build to 145.0.7632.159.6 — fix persistent context detection by FingerprintJS
- **[binary]** Storage quota normalization for persistent context profiles
- **[binary]** Fix outerHeight calculation for non-incognito contexts
- **[wrapper]** Add CLI for binary management — `python -m cloakbrowser install` / `npx cloakbrowser install` with visible download progress (closes #43)
## [0.3.13] — 2026-03-10
- **[wrapper]** Suppress Playwright's `--enable-unsafe-swiftshader` default arg — eliminates SwiftShader software renderer detection signal, letting the binary's GPU spoofing work cleanly
- **[binary]** Upgrade Linux build to 145.0.7632.159.5 — fix WebGPU adapter limits and features for NVIDIA profiles
## [0.3.12] — 2026-03-10 ## [0.3.12] — 2026-03-10
- **[binary]** Upgrade Linux build to 145.0.7632.159.4 - **[binary]** Upgrade Linux build to 145.0.7632.159.4
+102 -30
View File
@@ -14,6 +14,10 @@
<a href="https://hub.docker.com/r/cloakhq/cloakbrowser"><img src="https://img.shields.io/docker/pulls/cloakhq/cloakbrowser?label=docker&logo=docker&logoColor=white" alt="Docker Pulls"></a> <a href="https://hub.docker.com/r/cloakhq/cloakbrowser"><img src="https://img.shields.io/docker/pulls/cloakhq/cloakbrowser?label=docker&logo=docker&logoColor=white" alt="Docker Pulls"></a>
</p> </p>
<p align="center">
<a href="https://ko-fi.com/cloakhq"><img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support on Ko-fi"></a>
</p>
<br> <br>
<h3 align="center">Stealth Chromium that passes every bot detection test.</h3> <h3 align="center">Stealth Chromium that passes every bot detection test.</h3>
@@ -36,7 +40,7 @@ Drop-in Playwright/Puppeteer replacement for Python and JavaScript.<br>
Same API, same code — just swap the import. <strong>3 lines of code, 30 seconds to unblock.</strong> Same API, same code — just swap the import. <strong>3 lines of code, 30 seconds to unblock.</strong>
</p> </p>
- **32 source-level C++ patches** — canvas, WebGL, audio, fonts, GPU, screen, automation signals, CDP input behavior - **33 source-level C++ patches** — canvas, WebGL, audio, fonts, GPU, screen, automation signals, CDP input behavior
- **`humanize=True`** — human-like mouse curves, keyboard timing, and scroll patterns. One flag, behavioral detection passes - **`humanize=True`** — human-like mouse curves, keyboard timing, and scroll patterns. One flag, behavioral detection passes
- **0.9 reCAPTCHA v3 score** — human-level, server-verified - **0.9 reCAPTCHA v3 score** — human-level, server-verified
- **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — tested against 30+ detection sites - **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — tested against 30+ detection sites
@@ -110,13 +114,27 @@ page.goto("https://example.com")
> ⭐ **Star** to show support — **[Watch releases](https://github.com/CloakHQ/CloakBrowser/subscription)** to get notified when new builds drop. > ⭐ **Star** to show support — **[Watch releases](https://github.com/CloakHQ/CloakBrowser/subscription)** to get notified when new builds drop.
## Latest: v0.3.12 (Chromium 145.0.7632.159.4) ## Browser Profile Manager
Self-hosted alternative to Multilogin, GoLogin, and AdsPower. Create browser profiles with unique fingerprints, proxies, and persistent sessions. Launch and interact with them in your browser via noVNC.
```bash
docker run -p 8080:8080 -v cloakprofiles:/data cloakhq/cloakbrowser-manager
```
Open [http://localhost:8080](http://localhost:8080). Create a profile. Click **Launch**. Done.
**[CloakBrowser Manager](https://github.com/CloakHQ/CloakBrowser-Manager)** — free, open source (MIT)
---
## Latest: v0.3.15 (Chromium 145.0.7632.159.7)
- **`humanize=True`** — one flag makes all mouse, keyboard, and scroll interactions behave like a real user. Bézier curves, per-character typing, realistic scroll patterns. Two presets: `default` and `careful` - **`humanize=True`** — one flag makes all mouse, keyboard, and scroll interactions behave like a real user. Bézier curves, per-character typing, realistic scroll patterns. Two presets: `default` and `careful`
- **CDP input behavior mimicking** — input events sent via CDP now produce the same signals as real user interactions. 5 new source-level patches covering pointer, keyboard, and mouse behavior - **CDP input behavior mimicking** — input events sent via CDP now produce the same signals as real user interactions. 5 new source-level patches covering pointer, keyboard, and mouse behavior
- **Native locale spoofing** — new C++ patch replaces detectable CDP-level locale emulation - **Native locale spoofing** — new C++ patch replaces detectable CDP-level locale emulation
- **WebGPU fingerprint hardening** — adapter features, limits, and device ID spoofed for cross-API consistency - **WebGPU fingerprint hardening** — adapter features, limits, and device ID spoofed for cross-API consistency
- **32 fingerprint patches** (Linux x64) — all 4 platforms on Chromium 145 - **33 fingerprint patches** (Linux x64) — all 4 platforms on Chromium 145
- **Stealthy with zero flags** — binary auto-generates a random fingerprint seed at startup. No configuration required - **Stealthy with zero flags** — binary auto-generates a random fingerprint seed at startup. No configuration required
- **Timezone & locale from proxy IP** — `launch(proxy="...", geoip=True)` auto-detects timezone and locale - **Timezone & locale from proxy IP** — `launch(proxy="...", geoip=True)` auto-detects timezone and locale
- **Persistent profiles** — `launch_persistent_context()` keeps cookies and localStorage across sessions, bypasses incognito detection - **Persistent profiles** — `launch_persistent_context()` keeps cookies and localStorage across sessions, bypasses incognito detection
@@ -203,7 +221,7 @@ CloakBrowser is a thin wrapper (Python + JavaScript) around a custom-built Chrom
3. **Every launch** → Playwright or Puppeteer starts with our binary + stealth args 3. **Every launch** → Playwright or Puppeteer starts with our binary + stealth args
4. **You write code** → standard Playwright/Puppeteer API, nothing new to learn 4. **You write code** → standard Playwright/Puppeteer API, nothing new to learn
The binary includes 32 source-level patches covering canvas, WebGL, audio, fonts, GPU, screen properties, hardware reporting, automation signal removal, and CDP input behavior mimicking. The binary includes 33 source-level patches covering canvas, WebGL, audio, fonts, GPU, screen properties, hardware reporting, automation signal removal, and CDP input behavior mimicking.
These are compiled into the Chromium binary — not injected via JavaScript, not set via flags. These are compiled into the Chromium binary — not injected via JavaScript, not set via flags.
@@ -288,7 +306,7 @@ context.close()
### `launch_persistent_context()` ### `launch_persistent_context()`
Same as `launch_context()`, but with a persistent user profile. Cookies, localStorage, and cache persist across sessions. Also avoids incognito detection by services like BrowserScan. Same as `launch_context()`, but with a persistent user profile. Cookies, localStorage, and cache persist across sessions.
Use this when you need to: Use this when you need to:
- **Stay logged in** across runs (cookies/sessions survive restarts) - **Stay logged in** across runs (cookies/sessions survive restarts)
@@ -313,6 +331,28 @@ Supports all the same options as `launch_context()`: `proxy`, `user_agent`, `vie
Async version: `launch_persistent_context_async()`. Async version: `launch_persistent_context_async()`.
**Storage quota and detection tradeoff:** By default, the binary normalizes storage quota to pass FingerprintJS, which blocks persistent contexts that report non-incognito quota values. This means detection services that penalize incognito mode (like BrowserScan's `notPrivate` check, -10 points) will still flag it. If your target site penalizes incognito but doesn't use FingerprintJS, set a higher quota to appear as a regular profile:
```python
ctx = launch_persistent_context("./my-profile", args=["--fingerprint-storage-quota=5000"])
```
| Quota setting | FingerprintJS | BrowserScan `notPrivate` |
|---|---|---|
| Default (auto, ~500MB) | PASS | -10 (flagged as incognito) |
| `--fingerprint-storage-quota=5000` | May trigger detection | PASS (appears non-incognito) |
### CLI
Pre-download the binary or check installation status from the command line:
```bash
python -m cloakbrowser install # Download binary with progress output
python -m cloakbrowser info # Show version, path, platform
python -m cloakbrowser update # Check for and download newer binary
python -m cloakbrowser clear-cache # Remove cached binaries
```
### Utility Functions ### Utility Functions
```python ```python
@@ -528,6 +568,7 @@ Supported by the binary but **not set by default** — pass via `args` to custom
| `--fingerprint-location` | Geolocation coordinates | | `--fingerprint-location` | Geolocation coordinates |
| `--fingerprint-timezone` | Timezone (e.g. `America/New_York`) | | `--fingerprint-timezone` | Timezone (e.g. `America/New_York`) |
| `--fingerprint-locale` | Locale (e.g. `en-US`) | | `--fingerprint-locale` | Locale (e.g. `en-US`) |
| `--fingerprint-storage-quota` | Override storage quota in MB — affects `storage.estimate()`, `storageBuckets`, and legacy webkit APIs. Auto-normalized when `--fingerprint` is set |
| `--fingerprint-taskbar-height` | Override taskbar height (binary defaults: Win=48, Mac=95, Linux=0) | | `--fingerprint-taskbar-height` | Override taskbar height (binary defaults: Win=48, Mac=95, Linux=0) |
| `--fingerprint-fonts-dir` | Path to cross-platform font directory | | `--fingerprint-fonts-dir` | Path to cross-platform font directory |
| `--enable-blink-features=FakeShadowRoot` | Access closed shadow DOM elements | | `--enable-blink-features=FakeShadowRoot` | Access closed shadow DOM elements |
@@ -706,6 +747,15 @@ COPY your_script.py /app/
CMD ["python", "your_script.py"] CMD ["python", "your_script.py"]
``` ```
**Building your own image from pip** — use `python -m cloakbrowser install` to download the binary during build with visible progress:
```dockerfile
FROM python:3.12-slim
RUN pip install cloakbrowser && python -m cloakbrowser install
COPY your_script.py /app/
CMD ["python", "/app/your_script.py"]
```
**Building from source** — a [`Dockerfile`](Dockerfile) is also included if you prefer to build your own image: **Building from source** — a [`Dockerfile`](Dockerfile) is also included if you prefer to build your own image:
```bash ```bash
@@ -718,7 +768,9 @@ CloakBrowser works identically local, in Docker, and on VPS. No environment-spec
## Troubleshooting ## Troubleshooting
**Still getting blocked on aggressive sites (DataDome, Turnstile)?** ---
### Still getting blocked on aggressive sites (DataDome, Turnstile)?
Some sites detect headless mode even with our C++ patches. Run in **headed mode** with a virtual display: Some sites detect headless mode even with our C++ patches. Run in **headed mode** with a virtual display:
@@ -743,7 +795,9 @@ browser.close()
This runs a real headed browser rendered on a virtual display — no physical monitor needed. Combined with a residential proxy, this passes even the most aggressive detection services. Datacenter IPs are often flagged by IP reputation regardless of browser fingerprint — a residential proxy makes the difference. This runs a real headed browser rendered on a virtual display — no physical monitor needed. Combined with a residential proxy, this passes even the most aggressive detection services. Datacenter IPs are often flagged by IP reputation regardless of browser fingerprint — a residential proxy makes the difference.
**Sites challenge fresh sessions but work after first visit** ---
### Sites challenge fresh sessions but work after first visit
Some sites challenge first-time visitors with no cookies over HTTP/2. This affects all Chromium browsers, not just CloakBrowser. Use a persistent profile to warm up cookies once, then reuse across sessions: Some sites challenge first-time visitors with no cookies over HTTP/2. This affects all Chromium browsers, not just CloakBrowser. Use a persistent profile to warm up cookies once, then reuse across sessions:
@@ -777,7 +831,10 @@ ctx = await launchPersistentContext({ userDataDir: './profile' });
For stateless/ephemeral use cases, `launch(args=["--disable-http2"])` forces HTTP/1.1 which bypasses the check. Only use this flag for sites that require it — most work fine with HTTP/2. For stateless/ephemeral use cases, `launch(args=["--disable-http2"])` forces HTTP/1.1 which bypasses the check. Only use this flag for sites that require it — most work fine with HTTP/2.
**Something not working? Make sure you're on the latest version** ---
### Something not working? Make sure you're on the latest version
Older versions may use outdated stealth args or download an older binary: Older versions may use outdated stealth args or download an older binary:
```bash ```bash
pip install -U cloakbrowser # Python pip install -U cloakbrowser # Python
@@ -785,13 +842,19 @@ npm install cloakbrowser@latest # JavaScript
docker pull cloakhq/cloakbrowser:latest # Docker docker pull cloakhq/cloakbrowser:latest # Docker
``` ```
**Binary download fails / timeout** ---
### Binary download fails / timeout
Set a custom download URL or use a local binary: Set a custom download URL or use a local binary:
```bash ```bash
export CLOAKBROWSER_BINARY_PATH=/path/to/your/chrome export CLOAKBROWSER_BINARY_PATH=/path/to/your/chrome
``` ```
**New update broke something? Roll back to the previous version** ---
### New update broke something? Roll back to the previous version
Install a specific wrapper version to downgrade both the wrapper and the binary it downloads: Install a specific wrapper version to downgrade both the wrapper and the binary it downloads:
```bash ```bash
pip install cloakbrowser==0.3.11 # Python pip install cloakbrowser==0.3.11 # Python
@@ -800,45 +863,47 @@ docker pull cloakhq/cloakbrowser:0.3.11 # Docker
``` ```
Each wrapper version pins its own binary version, so downgrading the wrapper automatically gets you the matching binary on next launch. Each wrapper version pins its own binary version, so downgrading the wrapper automatically gets you the matching binary on next launch.
**macOS: "App is damaged" or Gatekeeper blocks launch** ---
### macOS: "App is damaged" or Gatekeeper blocks launch
The binary is ad-hoc signed. macOS quarantines downloaded files. Run once to clear it: The binary is ad-hoc signed. macOS quarantines downloaded files. Run once to clear it:
```bash ```bash
xattr -cr ~/.cloakbrowser/chromium-*/Chromium.app xattr -cr ~/.cloakbrowser/chromium-*/Chromium.app
``` ```
**"playwright install" vs CloakBrowser binary** ---
### "playwright install" vs CloakBrowser binary
You do NOT need `playwright install chromium`. CloakBrowser downloads its own binary. You only need Playwright's system deps: You do NOT need `playwright install chromium`. CloakBrowser downloads its own binary. You only need Playwright's system deps:
```bash ```bash
playwright install-deps chromium playwright install-deps chromium
``` ```
**macOS: Blocked on some sites that pass on Linux** ---
### macOS: Blocked on some sites that pass on Linux
The macOS fingerprint profile has known inconsistencies that aggressive bot detection catches. If a site blocks you on macOS but works on Linux, switch to a Windows fingerprint profile by passing `stealth_args=False` and manually setting `--fingerprint-platform=windows` with matching GPU flags (see [Fingerprint Management](#fingerprint-management) for the full flag list). The macOS fingerprint profile has known inconsistencies that aggressive bot detection catches. If a site blocks you on macOS but works on Linux, switch to a Windows fingerprint profile by passing `stealth_args=False` and manually setting `--fingerprint-platform=windows` with matching GPU flags (see [Fingerprint Management](#fingerprint-management) for the full flag list).
**Site detects incognito / private browsing mode** ---
By default, `launch()` opens an incognito context. Some sites (like BrowserScan) detect this. Use `launch_persistent_context()` instead — it runs with a real user profile, so incognito detection passes: ### Site detects incognito / private browsing mode
By default, `launch()` opens an incognito context. Some sites penalize this. Use `launch_persistent_context()` to get a real profile with cookie persistence:
```python ```python
from cloakbrowser import launch_persistent_context from cloakbrowser import launch_persistent_context
ctx = launch_persistent_context("./my-profile", headless=False) ctx = launch_persistent_context("./my-profile", headless=False)
page = ctx.new_page()
``` ```
```javascript If the site still flags incognito, raise the storage quota to appear as a regular browsing session. See the [storage quota tradeoff](#launch_persistent_context) for details on how this affects different detection services.
import { launchPersistentContext } from 'cloakbrowser';
const ctx = await launchPersistentContext({ ---
userDataDir: './my-profile',
headless: false,
});
```
This also gives you cookie and localStorage persistence across sessions. ### reCAPTCHA v3 scores are low (0.10.3)
**reCAPTCHA v3 scores are low (0.10.3)**
Avoid `page.wait_for_timeout()` — it sends CDP protocol commands that reCAPTCHA detects. Use native sleep instead: Avoid `page.wait_for_timeout()` — it sends CDP protocol commands that reCAPTCHA detects. Use native sleep instead:
@@ -904,19 +969,26 @@ A: Yes. Pass `proxy="http://user:pass@host:port"` to `launch()`.
- 🐛 **Bug reports & feature requests** — [GitHub Issues](https://github.com/CloakHQ/CloakBrowser/issues) - 🐛 **Bug reports & feature requests** — [GitHub Issues](https://github.com/CloakHQ/CloakBrowser/issues)
- 📦 **PyPI** — [pypi.org/project/cloakbrowser](https://pypi.org/project/cloakbrowser/) - 📦 **PyPI** — [pypi.org/project/cloakbrowser](https://pypi.org/project/cloakbrowser/)
- 📦 **npm** — [npmjs.com/package/cloakbrowser](https://www.npmjs.com/package/cloakbrowser) - 📦 **npm** — [npmjs.com/package/cloakbrowser](https://www.npmjs.com/package/cloakbrowser)
- ☕ **Support** — [ko-fi.com/cloakhq](https://ko-fi.com/cloakhq)
- 📧 **Contact** — cloakhq@pm.me - 📧 **Contact** — cloakhq@pm.me
## Security ## Security
All binary releases are GPG-signed and include GitHub artifact attestations for supply chain verification. All releases are signed for supply chain verification.
```bash ```bash
# Verify GPG signature # Verify GPG signature (binary release tag)
gpg --keyserver keyserver.ubuntu.com --recv-keys C60C0DDC9D0DE2DD gpg --keyserver keyserver.ubuntu.com --recv-keys C60C0DDC9D0DE2DD
git verify-tag chromium-v145.0.7632.159.4 git verify-tag chromium-v145.0.7632.159.7
# Verify binary attestation # Verify GitHub binary attestation (Sigstore)
gh attestation verify cloakbrowser-linux-x64.tar.gz --repo CloakHQ/cloakbrowser gh attestation verify cloakbrowser-linux-x64.tar.gz --repo CloakHQ/cloakbrowser
# Verify Docker image signature (Cosign/Sigstore)
cosign verify \
--certificate-identity-regexp "https://github.com/CloakHQ/CloakBrowser/" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
cloakhq/cloakbrowser:latest
``` ```
## License ## License
+111
View File
@@ -0,0 +1,111 @@
"""CLI for cloakbrowser — download and manage the stealth Chromium binary.
Usage:
python -m cloakbrowser install # Download binary (with progress)
python -m cloakbrowser info # Show binary version, path, platform
python -m cloakbrowser update # Check for and download newer binary
python -m cloakbrowser clear-cache # Remove cached binaries
"""
from __future__ import annotations
import argparse
import logging
import sys
def _setup_logging() -> None:
"""Route cloakbrowser logger to stderr with clean output."""
logging.basicConfig(
level=logging.INFO,
format="%(message)s",
stream=sys.stderr,
force=True,
)
# Suppress noisy HTTP request logs from httpx
logging.getLogger("httpx").setLevel(logging.WARNING)
def cmd_install(args: argparse.Namespace) -> None:
from .download import ensure_binary
path = ensure_binary()
print(path)
def cmd_info(args: argparse.Namespace) -> None:
from .config import get_local_binary_override
from .download import binary_info
info = binary_info()
override = get_local_binary_override()
print(f"Version: {info['version']}")
print(f"Platform: {info['platform']}")
print(f"Binary: {info['binary_path']}")
print(f"Installed: {info['installed']}")
print(f"Cache: {info['cache_dir']}")
if override:
print(f"Override: {override} (CLOAKBROWSER_BINARY_PATH)")
def cmd_update(args: argparse.Namespace) -> None:
from .download import check_for_update
logger = logging.getLogger("cloakbrowser")
logger.info("Checking for updates...")
new_version = check_for_update()
if new_version:
print(f"Updated to Chromium {new_version}")
else:
print("Already up to date.")
def cmd_clear_cache(args: argparse.Namespace) -> None:
from .config import get_cache_dir
from .download import clear_cache
if not get_cache_dir().exists():
print("No cache to clear.")
return
clear_cache()
print("Cache cleared.")
def main() -> None:
parser = argparse.ArgumentParser(
prog="cloakbrowser",
description="Manage the CloakBrowser stealth Chromium binary.",
)
sub = parser.add_subparsers(dest="command")
sub.add_parser("install", help="Download the Chromium binary")
sub.add_parser("info", help="Show binary version, path, and platform")
sub.add_parser("update", help="Check for and download a newer binary")
sub.add_parser("clear-cache", help="Remove all cached binaries")
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(2)
_setup_logging()
commands = {
"install": cmd_install,
"info": cmd_info,
"update": cmd_update,
"clear-cache": cmd_clear_cache,
}
try:
commands[args.command](args)
except KeyboardInterrupt:
sys.exit(130)
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.3.12" __version__ = "0.3.15"
+5 -5
View File
@@ -19,7 +19,7 @@ import os
from typing import Any, Literal, TypedDict from typing import Any, Literal, TypedDict
from urllib.parse import unquote, urlparse, urlunparse from urllib.parse import unquote, urlparse, urlunparse
from .config import DEFAULT_VIEWPORT, get_default_stealth_args from .config import DEFAULT_VIEWPORT, IGNORE_DEFAULT_ARGS, get_default_stealth_args
from .download import ensure_binary from .download import ensure_binary
logger = logging.getLogger("cloakbrowser") logger = logging.getLogger("cloakbrowser")
@@ -111,7 +111,7 @@ def launch(
executable_path=binary_path, executable_path=binary_path,
headless=headless, headless=headless,
args=chrome_args, args=chrome_args,
ignore_default_args=["--enable-automation"], ignore_default_args=IGNORE_DEFAULT_ARGS,
**_build_proxy_kwargs(proxy), **_build_proxy_kwargs(proxy),
**kwargs, **kwargs,
) )
@@ -194,7 +194,7 @@ async def launch_async( # noqa: C901
executable_path=binary_path, executable_path=binary_path,
headless=headless, headless=headless,
args=chrome_args, args=chrome_args,
ignore_default_args=["--enable-automation"], ignore_default_args=IGNORE_DEFAULT_ARGS,
**_build_proxy_kwargs(proxy), **_build_proxy_kwargs(proxy),
**kwargs, **kwargs,
) )
@@ -305,7 +305,7 @@ def launch_persistent_context(
executable_path=binary_path, executable_path=binary_path,
headless=headless, headless=headless,
args=chrome_args, args=chrome_args,
ignore_default_args=["--enable-automation"], ignore_default_args=IGNORE_DEFAULT_ARGS,
**_build_proxy_kwargs(proxy), **_build_proxy_kwargs(proxy),
**context_kwargs, **context_kwargs,
) )
@@ -418,7 +418,7 @@ async def launch_persistent_context_async(
executable_path=binary_path, executable_path=binary_path,
headless=headless, headless=headless,
args=chrome_args, args=chrome_args,
ignore_default_args=["--enable-automation"], ignore_default_args=IGNORE_DEFAULT_ARGS,
**_build_proxy_kwargs(proxy), **_build_proxy_kwargs(proxy),
**context_kwargs, **context_kwargs,
) )
+10 -2
View File
@@ -15,15 +15,23 @@ from ._version import __version__
# CHROMIUM_VERSION is the latest across all platforms (for display/reference). # CHROMIUM_VERSION is the latest across all platforms (for display/reference).
# Use get_chromium_version() for the current platform's actual version. # Use get_chromium_version() for the current platform's actual version.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
CHROMIUM_VERSION = "145.0.7632.159.4" CHROMIUM_VERSION = "145.0.7632.159.7"
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = { PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
"linux-x64": "145.0.7632.159.4", "linux-x64": "145.0.7632.159.7",
"darwin-arm64": "145.0.7632.109.2", "darwin-arm64": "145.0.7632.109.2",
"darwin-x64": "145.0.7632.109.2", "darwin-x64": "145.0.7632.109.2",
"windows-x64": "145.0.7632.109.2", "windows-x64": "145.0.7632.109.2",
} }
# ---------------------------------------------------------------------------
# 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. # Default stealth arguments passed to the patched Chromium binary.
# These activate source-level fingerprint patches compiled into the binary. # These activate source-level fingerprint patches compiled into the binary.
+9 -1
View File
@@ -41,7 +41,15 @@ def _get_nearby_key(ch: str) -> str:
def human_type(page: Any, raw: RawKeyboard, text: str, cfg: HumanConfig) -> None: def human_type(page: Any, raw: RawKeyboard, text: str, cfg: HumanConfig) -> None:
for i, ch in enumerate(text): for i, ch in enumerate(text):
# Mistype chance — press wrong key, notice, backspace, then correct # Non-ASCII characters (Cyrillic, CJK, emoji) — use insertText
if not ch.isascii():
sleep_ms(rand_range(cfg.key_hold))
raw.insert_text(ch)
if i < len(text) - 1:
_inter_char_delay(cfg)
continue
# Mistype chance — only for ASCII alphanumeric
if random.random() < cfg.mistype_chance and ch.isalnum(): if random.random() < cfg.mistype_chance and ch.isalnum():
wrong = _get_nearby_key(ch) wrong = _get_nearby_key(ch)
_type_normal_char(raw, wrong, cfg) _type_normal_char(raw, wrong, cfg)
+9 -1
View File
@@ -22,7 +22,15 @@ class AsyncRawKeyboard(Protocol):
async def async_human_type(page: Any, raw: AsyncRawKeyboard, text: str, cfg: HumanConfig) -> None: async def async_human_type(page: Any, raw: AsyncRawKeyboard, text: str, cfg: HumanConfig) -> None:
for i, ch in enumerate(text): for i, ch in enumerate(text):
# Mistype chance — press wrong key, notice, backspace, then correct # Non-ASCII characters (Cyrillic, CJK, emoji) — use insertText
if not ch.isascii():
await async_sleep_ms(rand_range(cfg.key_hold))
await raw.insert_text(ch)
if i < len(text) - 1:
await _inter_char_delay(cfg)
continue
# Mistype chance — only for ASCII alphanumeric
if random.random() < cfg.mistype_chance and ch.isalnum(): if random.random() < cfg.mistype_chance and ch.isalnum():
wrong = _get_nearby_key(ch) wrong = _get_nearby_key(ch)
await _type_normal_char(raw, wrong, cfg) await _type_normal_char(raw, wrong, cfg)
+12 -1
View File
@@ -11,7 +11,7 @@
Drop-in Playwright/Puppeteer replacement. Same API, same code — just swap the import. **3 lines of code, 30 seconds to unblock.** Drop-in Playwright/Puppeteer replacement. Same API, same code — just swap the import. **3 lines of code, 30 seconds to unblock.**
- **32 source-level C++ patches** — canvas, WebGL, audio, fonts, GPU, screen, automation signals - **33 source-level C++ patches** — canvas, WebGL, audio, fonts, GPU, screen, automation signals
- **0.9 reCAPTCHA v3 score** — human-level, server-verified - **0.9 reCAPTCHA v3 score** — human-level, server-verified
- **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — tested against 30+ detection sites - **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — tested against 30+ detection sites
- **`npm install cloakbrowser`** — binary auto-downloads, auto-updates, zero config - **`npm install cloakbrowser`** — binary auto-downloads, auto-updates, zero config
@@ -133,6 +133,17 @@ const browser = await launch({ proxy: 'http://proxy:8080', geoip: true, timezone
> **Note:** For rotating residential proxies, the DNS-resolved IP may differ from the exit IP. Pass explicit `timezone`/`locale` in those cases. > **Note:** For rotating residential proxies, the DNS-resolved IP may differ from the exit IP. Pass explicit `timezone`/`locale` in those cases.
### CLI
Pre-download the binary or check installation status from the command line:
```bash
npx cloakbrowser install # Download binary with progress output
npx cloakbrowser info # Show version, path, platform
npx cloakbrowser update # Check for and download newer binary
npx cloakbrowser clear-cache # Remove cached binaries
```
### Utilities ### Utilities
```javascript ```javascript
+4 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "cloakbrowser", "name": "cloakbrowser",
"version": "0.3.12", "version": "0.3.15",
"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",
@@ -15,6 +15,9 @@
"import": "./dist/puppeteer.js" "import": "./dist/puppeteer.js"
} }
}, },
"bin": {
"cloakbrowser": "./dist/cli.js"
},
"files": [ "files": [
"dist" "dist"
], ],
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env node
/**
* CLI for cloakbrowser — download and manage the stealth Chromium binary.
*
* Usage:
* npx cloakbrowser install # Download binary (with progress)
* npx cloakbrowser info # Show binary version, path, platform
* npx cloakbrowser update # Check for and download newer binary
* npx cloakbrowser clear-cache # Remove cached binaries
*/
import { ensureBinary, binaryInfo, checkForUpdate, clearCache } from "./download.js";
import { getLocalBinaryOverride, getCacheDir } from "./config.js";
import fs from "node:fs";
const USAGE = `Usage: cloakbrowser <command>
Commands:
install Download the Chromium binary
info Show binary version, path, and platform
update Check for and download a newer binary
clear-cache Remove all cached binaries`;
async function cmdInstall(): Promise<void> {
const binaryPath = await ensureBinary();
console.log(binaryPath);
}
function cmdInfo(): void {
const info = binaryInfo();
const override = getLocalBinaryOverride();
console.log(`Version: ${info.version}`);
console.log(`Platform: ${info.platform}`);
console.log(`Binary: ${info.binaryPath}`);
console.log(`Installed: ${info.installed}`);
console.log(`Cache: ${info.cacheDir}`);
if (override) {
console.log(`Override: ${override} (CLOAKBROWSER_BINARY_PATH)`);
}
}
async function cmdUpdate(): Promise<void> {
console.error("Checking for updates...");
const newVersion = await checkForUpdate();
if (newVersion) {
console.log(`Updated to Chromium ${newVersion}`);
} else {
console.log("Already up to date.");
}
}
function cmdClearCache(): void {
const cacheDir = getCacheDir();
if (!fs.existsSync(cacheDir)) {
console.log("No cache to clear.");
return;
}
clearCache();
console.log("Cache cleared.");
}
async function main(): Promise<void> {
const command = process.argv[2];
if (!command || command === "--help" || command === "-h") {
console.log(USAGE);
process.exit(command ? 0 : 2);
}
try {
switch (command) {
case "install":
await cmdInstall();
break;
case "info":
cmdInfo();
break;
case "update":
await cmdUpdate();
break;
case "clear-cache":
cmdClearCache();
break;
default:
console.error(`Unknown command: ${command}\n`);
console.log(USAGE);
process.exit(2);
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.error(`Error: ${message}`);
process.exit(1);
}
}
main();
+10 -2
View File
@@ -27,10 +27,10 @@ export { WRAPPER_VERSION };
// CHROMIUM_VERSION is the latest across all platforms (for display/reference). // CHROMIUM_VERSION is the latest across all platforms (for display/reference).
// Use getChromiumVersion() for the current platform's actual version. // Use getChromiumVersion() for the current platform's actual version.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
export const CHROMIUM_VERSION = "145.0.7632.159.4"; export const CHROMIUM_VERSION = "145.0.7632.159.7";
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = { export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
"linux-x64": "145.0.7632.159.4", "linux-x64": "145.0.7632.159.7",
"darwin-arm64": "145.0.7632.109.2", "darwin-arm64": "145.0.7632.109.2",
"darwin-x64": "145.0.7632.109.2", "darwin-x64": "145.0.7632.109.2",
"windows-x64": "145.0.7632.109.2", "windows-x64": "145.0.7632.109.2",
@@ -188,6 +188,14 @@ export function getLocalBinaryOverride(): string | undefined {
return process.env.CLOAKBROWSER_BINARY_PATH || undefined; return process.env.CLOAKBROWSER_BINARY_PATH || undefined;
} }
// ---------------------------------------------------------------------------
// 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
// ---------------------------------------------------------------------------
export const IGNORE_DEFAULT_ARGS = ["--enable-automation", "--enable-unsafe-swiftshader"];
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Default stealth arguments // Default stealth arguments
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
+2 -1
View File
@@ -227,7 +227,8 @@ async function verifyDownloadChecksum(filePath: string, version?: string): Promi
await verifyChecksum(filePath, expected); await verifyChecksum(filePath, expected);
} }
async function fetchChecksums(version?: string): Promise<Map<string, string> | null> { /** @internal Exported for testing only. */
export async function fetchChecksums(version?: string): Promise<Map<string, string> | null> {
const v = version || getChromiumVersion(); const v = version || getChromiumVersion();
const hasCustomUrl = !!process.env.CLOAKBROWSER_DOWNLOAD_URL; const hasCustomUrl = !!process.env.CLOAKBROWSER_DOWNLOAD_URL;
+22 -5
View File
@@ -22,6 +22,11 @@ const NEARBY_KEYS: Record<string, string> = {
'6': '57ty', '7': '68yu', '8': '79ui', '9': '80io', '0': '9p', '6': '57ty', '7': '68yu', '8': '79ui', '9': '80io', '0': '9p',
}; };
function isAscii(ch: string): boolean {
const code = ch.codePointAt(0);
return code !== undefined && code < 128;
}
function getNearbyKey(ch: string): string { function getNearbyKey(ch: string): string {
const lower = ch.toLowerCase(); const lower = ch.toLowerCase();
if (lower in NEARBY_KEYS) { if (lower in NEARBY_KEYS) {
@@ -38,11 +43,23 @@ export async function humanType(
text: string, text: string,
cfg: HumanConfig, cfg: HumanConfig,
): Promise<void> { ): Promise<void> {
for (let i = 0; i < text.length; i++) { const chars = [...text]; // Handle emoji surrogate pairs correctly
const ch = text[i];
// Mistype chance — press wrong key, notice, backspace, then correct for (let i = 0; i < chars.length; i++) {
if (Math.random() < cfg.mistype_chance && /[a-zA-Z0-9]/.test(ch)) { const ch = chars[i];
// Non-ASCII characters (Cyrillic, CJK, emoji) — use insertText
if (!isAscii(ch)) {
await sleep(randRange(cfg.key_hold));
await raw.insertText(ch);
if (i < chars.length - 1) {
await interCharDelay(cfg);
}
continue;
}
// Mistype chance — only for ASCII alphanumeric
if (Math.random() < cfg.mistype_chance && /^[a-zA-Z0-9]$/.test(ch)) {
const wrong = getNearbyKey(ch); const wrong = getNearbyKey(ch);
await typeNormalChar(raw, wrong, cfg); await typeNormalChar(raw, wrong, cfg);
await sleep(randRange(cfg.mistype_delay_notice)); await sleep(randRange(cfg.mistype_delay_notice));
@@ -60,7 +77,7 @@ export async function humanType(
await typeNormalChar(raw, ch, cfg); await typeNormalChar(raw, ch, cfg);
} }
if (i < text.length - 1) { if (i < chars.length - 1) {
await interCharDelay(cfg); await interCharDelay(cfg);
} }
} }
+3 -3
View File
@@ -5,7 +5,7 @@
import type { Browser, BrowserContext } from "playwright-core"; import type { Browser, BrowserContext } from "playwright-core";
import type { LaunchOptions, LaunchContextOptions, LaunchPersistentContextOptions } from "./types.js"; import type { LaunchOptions, LaunchContextOptions, LaunchPersistentContextOptions } from "./types.js";
import { DEFAULT_VIEWPORT } from "./config.js"; import { DEFAULT_VIEWPORT, IGNORE_DEFAULT_ARGS } from "./config.js";
import { buildArgs } from "./args.js"; import { buildArgs } from "./args.js";
import { ensureBinary } from "./download.js"; import { ensureBinary } from "./download.js";
import { parseProxyUrl } from "./proxy.js"; import { parseProxyUrl } from "./proxy.js";
@@ -45,7 +45,7 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
executablePath: binaryPath, executablePath: binaryPath,
headless: options.headless ?? true, headless: options.headless ?? true,
args, args,
ignoreDefaultArgs: ["--enable-automation"], ignoreDefaultArgs: IGNORE_DEFAULT_ARGS,
...(options.proxy ...(options.proxy
? { proxy: typeof options.proxy === "string" ? parseProxyUrl(options.proxy) : options.proxy } ? { proxy: typeof options.proxy === "string" ? parseProxyUrl(options.proxy) : options.proxy }
: {}), : {}),
@@ -163,7 +163,7 @@ export async function launchPersistentContext(
executablePath: binaryPath, executablePath: binaryPath,
headless: options.headless ?? true, headless: options.headless ?? true,
args, args,
ignoreDefaultArgs: ["--enable-automation"], ignoreDefaultArgs: IGNORE_DEFAULT_ARGS,
...(options.proxy ...(options.proxy
? { proxy: typeof options.proxy === "string" ? parseProxyUrl(options.proxy) : options.proxy } ? { proxy: typeof options.proxy === "string" ? parseProxyUrl(options.proxy) : options.proxy }
: {}), : {}),
+2 -1
View File
@@ -5,6 +5,7 @@
import type { Browser } from "puppeteer-core"; import type { Browser } from "puppeteer-core";
import type { LaunchOptions } from "./types.js"; import type { LaunchOptions } from "./types.js";
import { IGNORE_DEFAULT_ARGS } from "./config.js";
import { buildArgs } from "./args.js"; import { buildArgs } from "./args.js";
import { ensureBinary } from "./download.js"; import { ensureBinary } from "./download.js";
import { parseProxyUrl } from "./proxy.js"; import { parseProxyUrl } from "./proxy.js";
@@ -62,7 +63,7 @@ export async function launch(options: LaunchOptions = {}): Promise<Browser> {
executablePath: binaryPath, executablePath: binaryPath,
headless: options.headless ?? true, headless: options.headless ?? true,
args, args,
ignoreDefaultArgs: ["--enable-automation"], ignoreDefaultArgs: IGNORE_DEFAULT_ARGS,
...options.launchOptions, ...options.launchOptions,
}); });
+75
View File
@@ -530,6 +530,81 @@ function buildMockPage(overrides: Record<string, any> = {}): any {
return page; return page;
} }
// =========================================================================
// humanType non-ASCII
// =========================================================================
describe("humanType non-ASCII", () => {
function makeRawKeyboardMock() {
const downKeys: string[] = [];
const insertedChars: string[] = [];
const raw = {
down: vi.fn(async (k: string) => { downKeys.push(k); }),
up: vi.fn(async () => {}),
type: vi.fn(async () => {}),
insertText: vi.fn(async (t: string) => { insertedChars.push(t); }),
};
return { raw, downKeys, insertedChars };
}
it("types Cyrillic via insertText, not down", async () => {
const { humanType } = await import("../src/human/keyboard.js");
const cfg = resolveConfig("default", { mistype_chance: 0 });
const { raw, downKeys, insertedChars } = makeRawKeyboardMock();
await humanType({} as any, raw, "Привет", cfg);
expect(insertedChars.join("")).toBe("Привет");
for (const k of downKeys) {
expect(k.charCodeAt(0)).toBeLessThan(128);
}
});
it("types mixed ASCII + Cyrillic correctly", async () => {
const { humanType } = await import("../src/human/keyboard.js");
const cfg = resolveConfig("default", { mistype_chance: 0 });
const { raw, downKeys, insertedChars } = makeRawKeyboardMock();
await humanType({} as any, raw, "Hi Мир", cfg);
expect(downKeys).toContain("H");
expect(downKeys).toContain("i");
expect(insertedChars.join("")).toContain("М");
expect(insertedChars.join("")).toContain("и");
expect(insertedChars.join("")).toContain("р");
});
it("types CJK via insertText", async () => {
const { humanType } = await import("../src/human/keyboard.js");
const cfg = resolveConfig("default", { mistype_chance: 0 });
const { raw, insertedChars } = makeRawKeyboardMock();
await humanType({} as any, raw, "你好", cfg);
expect(insertedChars.join("")).toBe("你好");
});
it("types emoji via insertText", async () => {
const { humanType } = await import("../src/human/keyboard.js");
const cfg = resolveConfig("default", { mistype_chance: 0 });
const { raw, insertedChars } = makeRawKeyboardMock();
await humanType({} as any, raw, "Hi 👋", cfg);
expect(insertedChars.join("")).toContain("👋");
});
it("mistype only triggers for ASCII, not Cyrillic", async () => {
const { humanType } = await import("../src/human/keyboard.js");
const cfg = resolveConfig("default", { mistype_chance: 1.0 });
const { raw, downKeys } = makeRawKeyboardMock();
await humanType({} as any, raw, "AБ", cfg);
expect(downKeys).toContain("Backspace");
});
});
function buildMockFrame(): any { function buildMockFrame(): any {
return { return {
+49
View File
@@ -14,6 +14,7 @@ import {
checkWrapperUpdate, checkWrapperUpdate,
clearCache, clearCache,
ensureBinary, ensureBinary,
fetchChecksums,
getLatestChromiumVersion, getLatestChromiumVersion,
parseChecksums, parseChecksums,
resetWrapperUpdateChecked, resetWrapperUpdateChecked,
@@ -269,6 +270,54 @@ describe("parseChecksums", () => {
}); });
}); });
describe("download fallback", () => {
afterEach(() => {
vi.restoreAllMocks();
delete process.env.CLOAKBROWSER_DOWNLOAD_URL;
});
it("checksum fetch falls back to GitHub on primary 429", async () => {
const HASH =
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
const checksumText = `${HASH} cloakbrowser-${getPlatformTag()}.tar.gz`;
vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.toString()
: (input as Request).url;
if (url.includes("cloakbrowser.dev")) {
return {
ok: false,
status: 429,
statusText: "Too Many Requests",
} as Response;
}
// GitHub fallback
return { ok: true, text: async () => checksumText } as Response;
});
const result = await fetchChecksums();
expect(result).not.toBeNull();
expect(
result!.has(`cloakbrowser-${getPlatformTag()}.tar.gz`)
).toBe(true);
});
it("checksum fetch returns null when both sources fail", async () => {
vi.spyOn(globalThis, "fetch").mockResolvedValue({
ok: false,
status: 429,
statusText: "Too Many Requests",
} as Response);
const result = await fetchChecksums();
expect(result).toBeNull();
});
});
describe("effective version", () => { describe("effective version", () => {
it("returns platform version when no marker exists", () => { it("returns platform version when no marker exists", () => {
// Default behavior — no marker file in test environment // Default behavior — no marker file in test environment
+3
View File
@@ -58,6 +58,9 @@ geoip = ["geoip2>=4.0"]
patchright = ["patchright>=1.40"] patchright = ["patchright>=1.40"]
dev = ["pytest>=7.0", "pytest-asyncio>=0.23"] dev = ["pytest>=7.0", "pytest-asyncio>=0.23"]
[project.scripts]
cloakbrowser = "cloakbrowser.__main__:main"
[project.urls] [project.urls]
Homepage = "https://github.com/CloakHQ/CloakBrowser" Homepage = "https://github.com/CloakHQ/CloakBrowser"
Documentation = "https://github.com/CloakHQ/CloakBrowser#readme" Documentation = "https://github.com/CloakHQ/CloakBrowser#readme"
+121
View File
@@ -406,6 +406,127 @@ class TestSelectAllPlatform:
assert _SELECT_ALL == "Control+a" assert _SELECT_ALL == "Control+a"
# =========================================================================
# 11. Non-ASCII keyboard input
# =========================================================================
class TestNonAsciiKeyboard:
def test_cyrillic_uses_insert_text(self):
from cloakbrowser.human.keyboard import human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock
cfg = resolve_config("default", {"mistype_chance": 0})
page = MagicMock()
raw = MagicMock()
down_keys = []
inserted = []
raw.down = MagicMock(side_effect=lambda k: down_keys.append(k))
raw.up = MagicMock()
raw.insert_text = MagicMock(side_effect=lambda t: inserted.append(t))
human_type(page, raw, "Привет", cfg)
assert "".join(inserted) == "Привет"
for k in down_keys:
assert ord(k[0]) < 128 or k in ("Shift", "Backspace")
def test_mixed_ascii_cyrillic(self):
from cloakbrowser.human.keyboard import human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock
cfg = resolve_config("default", {"mistype_chance": 0})
page = MagicMock()
raw = MagicMock()
down_keys = []
inserted = []
raw.down = MagicMock(side_effect=lambda k: down_keys.append(k))
raw.up = MagicMock()
raw.insert_text = MagicMock(side_effect=lambda t: inserted.append(t))
human_type(page, raw, "Hi Мир", cfg)
assert "H" in down_keys
assert "i" in down_keys
assert "М" in "".join(inserted)
def test_cjk_uses_insert_text(self):
from cloakbrowser.human.keyboard import human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock
cfg = resolve_config("default", {"mistype_chance": 0})
page = MagicMock()
raw = MagicMock()
inserted = []
raw.down = MagicMock()
raw.up = MagicMock()
raw.insert_text = MagicMock(side_effect=lambda t: inserted.append(t))
human_type(page, raw, "你好", cfg)
assert "".join(inserted) == "你好"
def test_mistype_only_ascii(self):
from cloakbrowser.human.keyboard import human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock
cfg = resolve_config("default", {"mistype_chance": 1.0})
page = MagicMock()
raw = MagicMock()
down_keys = []
raw.down = MagicMock(side_effect=lambda k: down_keys.append(k))
raw.up = MagicMock()
raw.insert_text = MagicMock()
human_type(page, raw, "", cfg)
assert "Backspace" in down_keys
def test_no_error_on_cyrillic(self):
from cloakbrowser.human.keyboard import human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock
cfg = resolve_config("default", {"mistype_chance": 0})
page = MagicMock()
raw = MagicMock()
raw.down = MagicMock()
raw.up = MagicMock()
raw.insert_text = MagicMock()
# Should not raise
human_type(page, raw, "Тест кириллицы", cfg)
class TestNonAsciiKeyboardAsync:
@pytest.mark.asyncio
async def test_async_cyrillic_uses_insert_text(self):
from cloakbrowser.human.keyboard_async import async_human_type
from cloakbrowser.human.config import resolve_config
from unittest.mock import MagicMock, AsyncMock
cfg = resolve_config("default", {"mistype_chance": 0})
page = MagicMock()
raw = MagicMock()
inserted = []
raw.down = AsyncMock()
raw.up = AsyncMock()
raw.insert_text = AsyncMock(side_effect=lambda t: inserted.append(t))
await async_human_type(page, raw, "Привет", cfg)
assert "".join(inserted) == "Привет"
# ========================================================================= # =========================================================================
# SLOW TESTS — require browser (skipped in CI unless pytest -m slow) # SLOW TESTS — require browser (skipped in CI unless pytest -m slow)
# ========================================================================= # =========================================================================
+74
View File
@@ -20,6 +20,8 @@ from cloakbrowser.config import (
) )
from cloakbrowser.download import ( from cloakbrowser.download import (
_check_wrapper_update, _check_wrapper_update,
_download_and_extract,
_fetch_checksums,
_get_latest_chromium_version, _get_latest_chromium_version,
_parse_checksums, _parse_checksums,
_should_check_for_update, _should_check_for_update,
@@ -474,3 +476,75 @@ class TestWriteVersionMarker:
marker = tmp_path / f"latest_version_{get_platform_tag()}" marker = tmp_path / f"latest_version_{get_platform_tag()}"
assert marker.exists() assert marker.exists()
assert marker.read_text() == "999.0.0.0" assert marker.read_text() == "999.0.0.0"
class TestDownloadFallback:
"""Verify primary server (cloakbrowser.dev) → GitHub Releases fallback on HTTP errors."""
def test_binary_download_falls_back_on_http_error(self, tmp_path):
"""HTTP error from primary triggers GitHub Releases fallback for binary download."""
with patch.dict(os.environ, {
"CLOAKBROWSER_CACHE_DIR": str(tmp_path),
"CLOAKBROWSER_DOWNLOAD_URL": "",
"CLOAKBROWSER_SKIP_CHECKSUM": "true",
}):
urls_called = []
def mock_download_file(url, dest):
urls_called.append(url)
if "cloakbrowser.dev" in url:
raise Exception("HTTP 429 Too Many Requests")
# GitHub fallback succeeds
dest.write_bytes(b"fake")
with patch("cloakbrowser.download._download_file", side_effect=mock_download_file), \
patch("cloakbrowser.download._extract_archive"), \
patch("cloakbrowser.download._show_welcome"):
_download_and_extract()
assert len(urls_called) == 2
assert "cloakbrowser.dev" in urls_called[0]
assert "github.com" in urls_called[1]
def test_binary_download_no_fallback_with_custom_url(self, tmp_path):
"""Custom CLOAKBROWSER_DOWNLOAD_URL disables GitHub fallback — error propagates."""
with patch.dict(os.environ, {
"CLOAKBROWSER_CACHE_DIR": str(tmp_path),
"CLOAKBROWSER_DOWNLOAD_URL": "https://my-mirror.com/releases",
"CLOAKBROWSER_SKIP_CHECKSUM": "true",
}):
with patch("cloakbrowser.download._download_file", side_effect=Exception("503")):
with pytest.raises(Exception, match="503"):
_download_and_extract()
def test_checksum_fetch_falls_back_on_http_error(self):
"""HTTP error from primary checksum URL triggers GitHub fallback."""
valid_checksums = (
"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
" cloakbrowser-linux-x64.tar.gz\n"
)
def mock_get(url, **kwargs):
resp = MagicMock()
if "cloakbrowser.dev" in url:
resp.raise_for_status.side_effect = Exception("HTTP 429")
return resp
# GitHub URL succeeds
resp.text = valid_checksums
resp.raise_for_status = MagicMock()
return resp
with patch.dict(os.environ, {"CLOAKBROWSER_DOWNLOAD_URL": ""}):
with patch("cloakbrowser.download.httpx.get", side_effect=mock_get):
result = _fetch_checksums()
assert result is not None
assert "cloakbrowser-linux-x64.tar.gz" in result
def test_checksum_fetch_returns_none_when_both_fail(self):
"""Both primary and GitHub checksum URLs fail → returns None (skip verification)."""
with patch.dict(os.environ, {"CLOAKBROWSER_DOWNLOAD_URL": ""}):
with patch("cloakbrowser.download.httpx.get", side_effect=Exception("network error")):
result = _fetch_checksums()
assert result is None