mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d2a42fc86b | ||
|
|
1af25d67bc | ||
|
|
1bef989404 | ||
|
|
0aa4ea56bd | ||
|
|
c0ba21faa1 | ||
|
|
b501d8f158 | ||
|
|
6007a6e511 | ||
|
|
96c55352e0 | ||
|
|
5d35fb9e4c | ||
|
|
c966e046e7 |
@@ -0,0 +1 @@
|
|||||||
|
ko_fi: cloakhq
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.1–0.3)
|
||||||
|
|
||||||
**reCAPTCHA v3 scores are low (0.1–0.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
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
__version__ = "0.3.12"
|
__version__ = "0.3.15"
|
||||||
|
|||||||
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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, "AБ", 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)
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user