mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f46f8e9364 | ||
|
|
592b3d5661 | ||
|
|
a0a8210e35 | ||
|
|
468964ff30 | ||
|
|
c1b93e634b | ||
|
|
5ccb4a32a5 | ||
|
|
49d80d3b57 | ||
|
|
2813b3dc4c | ||
|
|
6550f3ad6c | ||
|
|
132cafe13c | ||
|
|
6c94b9e985 | ||
|
|
fdc1ae0484 | ||
|
|
2ded0c1866 | ||
|
|
f91700c4a4 | ||
|
|
1380c86847 | ||
|
|
83e3b30117 | ||
|
|
5649620545 | ||
|
|
d2a42fc86b | ||
|
|
1af25d67bc | ||
|
|
1bef989404 | ||
|
|
0aa4ea56bd |
@@ -0,0 +1 @@
|
|||||||
|
ko_fi: cloakhq
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
name: Bug Report
|
||||||
|
about: Report a bug or detection issue
|
||||||
|
labels: bug
|
||||||
|
---
|
||||||
|
|
||||||
|
Description: <!-- What happened? What did you expect? -->
|
||||||
|
|
||||||
|
CloakBrowser version: <!-- pip show cloakbrowser / npm list cloakbrowser -->
|
||||||
|
|
||||||
|
Wrapper: <!-- Python or JavaScript -->
|
||||||
|
|
||||||
|
Environment: <!-- OS, Docker y/n, base image, architecture -->
|
||||||
|
|
||||||
|
Launch options:
|
||||||
|
|
||||||
|
|
||||||
|
Tested with a different IP or proxy? <!-- Yes (same result) / Yes (works with different IP) / No -->
|
||||||
|
|
||||||
|
Works outside Docker / on host machine? <!-- Yes / No / Not using Docker -->
|
||||||
|
|
||||||
|
Steps to reproduce:
|
||||||
|
|
||||||
|
|
||||||
|
Error output / screenshots:
|
||||||
|
|
||||||
|
Dockerfile (if applicable):
|
||||||
|
|
||||||
|
Additional notes:
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
blank_issues_enabled: true
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "weekly"
|
||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
|
||||||
- name: Attest build provenance
|
- name: Attest build provenance
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
||||||
with:
|
with:
|
||||||
subject-path: |
|
subject-path: |
|
||||||
cloakbrowser-*.tar.gz
|
cloakbrowser-*.tar.gz
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ jobs:
|
|||||||
python:
|
python:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -22,8 +22,8 @@ jobs:
|
|||||||
javascript:
|
javascript:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: 20
|
||||||
- name: Install and build
|
- name: Install and build
|
||||||
|
|||||||
@@ -24,15 +24,15 @@ jobs:
|
|||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
- name: Python tests
|
- name: Python tests
|
||||||
run: |
|
run: |
|
||||||
pip install -e ".[dev]" pytest pytest-asyncio
|
pip install -e ".[dev]" pytest pytest-asyncio
|
||||||
pytest tests/ -v -m "not slow"
|
pytest tests/ -v -m "not slow"
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
- name: JavaScript tests
|
- name: JavaScript tests
|
||||||
@@ -42,8 +42,8 @@ jobs:
|
|||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
- name: Check tag matches package versions
|
- name: Check tag matches package versions
|
||||||
@@ -62,8 +62,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
id-token: write # OIDC trusted publishing — no PYPI_TOKEN needed
|
id-token: write # OIDC trusted publishing — no PYPI_TOKEN needed
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- uses: actions/setup-python@v5
|
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: "3.12"
|
||||||
- name: Build
|
- name: Build
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
pip install build
|
pip install build
|
||||||
python -m build
|
python -m build
|
||||||
- name: Publish to PyPI
|
- name: Publish to PyPI
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1
|
||||||
|
|
||||||
publish-npm:
|
publish-npm:
|
||||||
needs: [test, validate-version]
|
needs: [test, validate-version]
|
||||||
@@ -80,8 +80,8 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
id-token: write # OIDC trusted publishing + provenance — no NPM_TOKEN needed
|
id-token: write # OIDC trusted publishing + provenance — no NPM_TOKEN needed
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
@@ -102,32 +102,34 @@ jobs:
|
|||||||
attestations: write
|
attestations: write
|
||||||
packages: write
|
packages: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
- name: Extract version
|
- name: Extract version
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(python -c 'import re; print(re.search(r"__version__\s*=\s*[\"'\'']([^\"'\'']+)", open("cloakbrowser/_version.py").read()).group(1))')
|
VERSION=$(python -c 'import re; print(re.search(r"__version__\s*=\s*[\"'\'']([^\"'\'']+)", open("cloakbrowser/_version.py").read()).group(1))')
|
||||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||||
- uses: docker/setup-buildx-action@v3
|
- uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
|
||||||
- uses: docker/login-action@v3
|
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
||||||
|
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_USER }}
|
username: ${{ secrets.DOCKER_USER }}
|
||||||
password: ${{ secrets.DOCKER_PAT }}
|
password: ${{ secrets.DOCKER_PAT }}
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
cloakhq/cloakbrowser:${{ env.VERSION }}
|
cloakhq/cloakbrowser:${{ env.VERSION }}
|
||||||
cloakhq/cloakbrowser:latest
|
cloakhq/cloakbrowser:latest
|
||||||
provenance: true
|
provenance: true
|
||||||
sbom: true
|
sbom: true
|
||||||
- uses: sigstore/cosign-installer@v3
|
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||||
- name: Sign image
|
- name: Sign image
|
||||||
run: cosign sign --yes cloakhq/cloakbrowser@${{ steps.build.outputs.digest }}
|
run: cosign sign --yes cloakhq/cloakbrowser@${{ steps.build.outputs.digest }}
|
||||||
- name: Attest build provenance
|
- name: Attest build provenance
|
||||||
uses: actions/attest-build-provenance@v2
|
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
|
||||||
with:
|
with:
|
||||||
subject-name: index.docker.io/cloakhq/cloakbrowser
|
subject-name: index.docker.io/cloakhq/cloakbrowser
|
||||||
subject-digest: ${{ steps.build.outputs.digest }}
|
subject-digest: ${{ steps.build.outputs.digest }}
|
||||||
|
|||||||
@@ -6,6 +6,43 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.3.19] — 2026-03-30
|
||||||
|
|
||||||
|
- **[binary]** Upgrade Linux x64 build to 145.0.7632.159.8 — 42 source-level C++ patches (up from 33)
|
||||||
|
- **[binary]** 9 new fingerprint patches covering additional browser APIs and cross-platform consistency
|
||||||
|
- **[binary]** New `--fingerprint-noise` flag — disable noise injection while keeping deterministic fingerprint seed active
|
||||||
|
- **[binary]** Improved fingerprint noise reliability and determinism across all patched APIs
|
||||||
|
- **[binary]** Expanded platform-aware fingerprint spoofing for more realistic cross-platform profiles
|
||||||
|
- **[binary]** Font rendering and detection accuracy improvements for Windows profiles
|
||||||
|
- **[binary]** Removed experimental patches that caused compatibility issues with certain anti-bot systems
|
||||||
|
- **[binary]** Docker/VNC environment compatibility improvements
|
||||||
|
- **[wrapper]** Fix Playwright cleanup — `pw.stop()` now runs even if `browser.close()` raises or is cancelled (fixes #60, thanks [@dgtlmoon](https://github.com/dgtlmoon))
|
||||||
|
- **[meta]** Pin GitHub Actions to commit SHAs, add Dependabot for automated dependency updates
|
||||||
|
|
||||||
|
## [0.3.18] — 2026-03-15
|
||||||
|
|
||||||
|
- **[wrapper]** Fix welcome banner printing to stdout — now writes to stderr so it won't corrupt JSON output in programmatic usage (fixes #59)
|
||||||
|
- **[wrapper]** Fix `cloakserve` Docker WebGL by adding `--ignore-gpu-blocklist` flag
|
||||||
|
- **[docs]** Add Crawlee integration example
|
||||||
|
- **[meta]** Add GitHub issue template for bug reports
|
||||||
|
|
||||||
|
## [0.3.17] — 2026-03-15
|
||||||
|
|
||||||
|
- **[binary]** Windows x64 build upgraded to 145.0.7632.159.7 — 33 source-level C++ patches, matching Linux
|
||||||
|
- **[wrapper]** Auto-inject GPU blocklist bypass for headed mode and Windows — fixes WebGL/WebGPU on software GPUs in Docker/VNC (fixes #56)
|
||||||
|
- **[wrapper]** Add 8 framework integration examples (Scrapy, Crawlee, BrowserBase, etc.) and README integrations section
|
||||||
|
|
||||||
|
## [0.3.16] — 2026-03-14
|
||||||
|
|
||||||
|
- **[binary]** Linux arm64 build available — Raspberry Pi, AWS Graviton, Oracle Ampere now supported
|
||||||
|
- **[wrapper]** Add donate link to first-launch welcome banner
|
||||||
|
|
||||||
|
## [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
|
## [0.3.14] — 2026-03-12
|
||||||
|
|
||||||
- **[binary]** Upgrade Linux build to 145.0.7632.159.6 — fix persistent context detection by FingerprintJS
|
- **[binary]** Upgrade Linux build to 145.0.7632.159.6 — fix persistent context detection by FingerprintJS
|
||||||
|
|||||||
@@ -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.14 (Chromium 145.0.7632.159.6)
|
## 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.17 (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
|
||||||
@@ -129,7 +147,7 @@ See the full [CHANGELOG.md](CHANGELOG.md) for details.
|
|||||||
- **CloakBrowser patches Chromium source code** — fingerprints are modified at the C++ level, compiled into the binary. Detection sites see a real browser because it *is* a real browser.
|
- **CloakBrowser patches Chromium source code** — fingerprints are modified at the C++ level, compiled into the binary. Detection sites see a real browser because it *is* a real browser.
|
||||||
- **Source-level stealth** — C++ patches handle fingerprints (GPU, screen, UA, hardware reporting) at the binary level. No JavaScript injection, no config-level hacks. Most stealth tools only patch at the surface.
|
- **Source-level stealth** — C++ patches handle fingerprints (GPU, screen, UA, hardware reporting) at the binary level. No JavaScript injection, no config-level hacks. Most stealth tools only patch at the surface.
|
||||||
- **Same behavior everywhere** — works identically local, in Docker, and on VPS. No environment-specific patches or config needed.
|
- **Same behavior everywhere** — works identically local, in Docker, and on VPS. No environment-specific patches or config needed.
|
||||||
- **Works with AI agents and automation frameworks** — drop-in stealth for browser-use, Crawl4AI, agent-browser, Claude computer use, and OpenAI Operator. Also tested with Playwright, Puppeteer, and Selenium — point any Chromium-based framework at the binary path.
|
- **Works with AI agents and automation frameworks** — drop-in stealth for browser-use, Crawl4AI, Scrapling, Stagehand, LangChain, Selenium, and more. See [integrations](#framework-integrations).
|
||||||
|
|
||||||
CloakBrowser doesn't solve CAPTCHAs — it prevents them from appearing. No CAPTCHA-solving services, no proxy rotation built in — bring your own proxies, use the Playwright API you already know.
|
CloakBrowser doesn't solve CAPTCHAs — it prevents them from appearing. No CAPTCHA-solving services, no proxy rotation built in — bring your own proxies, use the Playwright API you already know.
|
||||||
|
|
||||||
@@ -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,17 @@ 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
|
### CLI
|
||||||
|
|
||||||
Pre-download the binary or check installation status from the command line:
|
Pre-download the binary or check installation status from the command line:
|
||||||
@@ -472,6 +501,8 @@ const browser = await launch({
|
|||||||
|
|
||||||
Access the original un-patched Playwright page at `page._original` if you need raw speed for a specific call.
|
Access the original un-patched Playwright page at `page._original` if you need raw speed for a specific call.
|
||||||
|
|
||||||
|
> **Note:** Always use `page.click(selector)`, `page.type(selector, text)`, `page.hover(selector)`, or `page.locator(selector).*` — these go through the full humanize pipeline. Avoid `page.query_selector()` — `ElementHandle` objects bypass all patches, so mouse movement teleports, keyboard events fire without timing, and scroll has no human curve.
|
||||||
|
|
||||||
> Contributed by [@evelaa123](https://github.com/evelaa123) — full Playwright API coverage.
|
> Contributed by [@evelaa123](https://github.com/evelaa123) — full Playwright API coverage.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
@@ -539,6 +570,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 |
|
||||||
@@ -580,14 +612,44 @@ browser = launch(args=[
|
|||||||
- [`basic-puppeteer.ts`](js/examples/basic-puppeteer.ts) — Puppeteer launch and load
|
- [`basic-puppeteer.ts`](js/examples/basic-puppeteer.ts) — Puppeteer launch and load
|
||||||
- [`stealth-test.ts`](js/examples/stealth-test.ts) — Run against 6 detection sites
|
- [`stealth-test.ts`](js/examples/stealth-test.ts) — Run against 6 detection sites
|
||||||
|
|
||||||
|
### Framework Integrations
|
||||||
|
|
||||||
|
CloakBrowser works with any framework that uses Playwright or Chromium:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# Option 1: Framework launches our binary directly (Selenium, Stagehand, UC)
|
||||||
|
from cloakbrowser.download import ensure_binary
|
||||||
|
from cloakbrowser.config import get_default_stealth_args
|
||||||
|
binary_path = ensure_binary() # auto-downloads if needed
|
||||||
|
stealth_args = get_default_stealth_args() # all fingerprint flags
|
||||||
|
|
||||||
|
# Option 2: CloakBrowser launches first, framework connects via CDP (browser-use, Crawl4AI, Scrapling)
|
||||||
|
from cloakbrowser import launch_async
|
||||||
|
browser = await launch_async(args=["--remote-debugging-port=9242"])
|
||||||
|
# Connect your framework to http://127.0.0.1:9242 — all stealth flags are set
|
||||||
|
```
|
||||||
|
|
||||||
|
| Framework | Stars | Language | Example |
|
||||||
|
|-----------|-------|----------|---------|
|
||||||
|
| [browser-use](https://github.com/browser-use/browser-use) | 70K | Python | [`browser_use_example.py`](examples/integrations/browser_use_example.py) |
|
||||||
|
| [Crawl4AI](https://github.com/unclecode/crawl4ai) | 58K | Python | [`crawl4ai_example.py`](examples/integrations/crawl4ai_example.py) |
|
||||||
|
| [Crawlee](https://github.com/apify/crawlee-python) | 8.6K | Python | [`crawlee_example.py`](examples/integrations/crawlee_example.py) |
|
||||||
|
| [Scrapling](https://github.com/D4Vinci/Scrapling) | 21K | Python | [`scrapling_example.py`](examples/integrations/scrapling_example.py) |
|
||||||
|
| [Stagehand](https://github.com/browserbase/stagehand) | 21K | TypeScript | [`stagehand.ts`](js/examples/stagehand.ts) |
|
||||||
|
| [LangChain](https://github.com/langchain-ai/langchain) | 100K+ | Python | [`langchain_loader.py`](examples/integrations/langchain_loader.py) |
|
||||||
|
| [Selenium](https://github.com/SeleniumHQ/selenium) | — | Python | [`selenium_example.py`](examples/integrations/selenium_example.py) |
|
||||||
|
| [undetected-chromedriver](https://github.com/ultrafunkamsterdam/undetected-chromedriver) | 12K | Python | [`undetected_chromedriver.py`](examples/integrations/undetected_chromedriver.py) |
|
||||||
|
| [agent-browser](https://github.com/nichochar/agent-browser) | — | Shell | [`agent_browser.sh`](examples/integrations/agent_browser.sh) |
|
||||||
|
|
||||||
## Platforms
|
## Platforms
|
||||||
|
|
||||||
| Platform | Chromium | Patches | Status |
|
| Platform | Chromium | Patches | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Linux x86_64 | 145 | 31 | ✅ Latest |
|
| Linux x86_64 | 145 | 33 | ✅ Latest |
|
||||||
|
| Linux arm64 (RPi, Graviton) | 145 | 33 | ✅ Latest |
|
||||||
| macOS arm64 (Apple Silicon) | 145 | 26 | ✅ Latest |
|
| macOS arm64 (Apple Silicon) | 145 | 26 | ✅ Latest |
|
||||||
| macOS x86_64 (Intel) | 145 | 26 | ✅ Latest |
|
| macOS x86_64 (Intel) | 145 | 26 | ✅ Latest |
|
||||||
| Windows x86_64 | 145 | 26 | ✅ Latest |
|
| Windows x86_64 | 145 | 33 | ✅ Latest |
|
||||||
|
|
||||||
The wrapper auto-downloads the correct binary for your platform.
|
The wrapper auto-downloads the correct binary for your platform.
|
||||||
|
|
||||||
@@ -861,25 +923,15 @@ The macOS fingerprint profile has known inconsistencies that aggressive bot dete
|
|||||||
|
|
||||||
### Site detects incognito / private browsing mode
|
### 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:
|
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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -949,6 +1001,7 @@ 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
|
||||||
@@ -958,7 +1011,7 @@ All releases are signed for supply chain verification.
|
|||||||
```bash
|
```bash
|
||||||
# Verify GPG signature (binary release tag)
|
# 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.6
|
git verify-tag chromium-v145.0.7632.159.7
|
||||||
|
|
||||||
# Verify GitHub binary attestation (Sigstore)
|
# 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
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ chrome_args = [
|
|||||||
"--disable-popup-blocking",
|
"--disable-popup-blocking",
|
||||||
"--disable-background-networking",
|
"--disable-background-networking",
|
||||||
"--metrics-recording-only",
|
"--metrics-recording-only",
|
||||||
|
# GPU blocklist bypass: Chromium blocks WebGL on software GPUs in
|
||||||
|
# Docker/Xvfb. Without this, WebGL vendor/renderer spoofing fails. #58
|
||||||
|
"--ignore-gpu-blocklist",
|
||||||
] + get_default_stealth_args() + sys.argv[1:]
|
] + get_default_stealth_args() + sys.argv[1:]
|
||||||
|
|
||||||
chrome = subprocess.Popen(chrome_args)
|
chrome = subprocess.Popen(chrome_args)
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.3.14"
|
__version__ = "0.3.19"
|
||||||
|
|||||||
+35
-14
@@ -102,7 +102,7 @@ def launch(
|
|||||||
|
|
||||||
binary_path = ensure_binary()
|
binary_path = ensure_binary()
|
||||||
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
||||||
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
|
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
||||||
|
|
||||||
logger.debug("Launching stealth Chromium (headless=%s, args=%d)", headless, len(chrome_args))
|
logger.debug("Launching stealth Chromium (headless=%s, args=%d)", headless, len(chrome_args))
|
||||||
|
|
||||||
@@ -120,8 +120,10 @@ def launch(
|
|||||||
_original_close = browser.close
|
_original_close = browser.close
|
||||||
|
|
||||||
def _close_with_cleanup() -> None:
|
def _close_with_cleanup() -> None:
|
||||||
_original_close()
|
try:
|
||||||
pw.stop()
|
_original_close()
|
||||||
|
finally:
|
||||||
|
pw.stop()
|
||||||
|
|
||||||
browser.close = _close_with_cleanup
|
browser.close = _close_with_cleanup
|
||||||
|
|
||||||
@@ -185,7 +187,7 @@ async def launch_async( # noqa: C901
|
|||||||
|
|
||||||
binary_path = ensure_binary()
|
binary_path = ensure_binary()
|
||||||
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
||||||
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
|
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
||||||
|
|
||||||
logger.debug("Launching stealth Chromium async (headless=%s, args=%d)", headless, len(chrome_args))
|
logger.debug("Launching stealth Chromium async (headless=%s, args=%d)", headless, len(chrome_args))
|
||||||
|
|
||||||
@@ -203,8 +205,10 @@ async def launch_async( # noqa: C901
|
|||||||
_original_close = browser.close
|
_original_close = browser.close
|
||||||
|
|
||||||
async def _close_with_cleanup() -> None:
|
async def _close_with_cleanup() -> None:
|
||||||
await _original_close()
|
try:
|
||||||
await pw.stop()
|
await _original_close()
|
||||||
|
finally:
|
||||||
|
await pw.stop()
|
||||||
|
|
||||||
browser.close = _close_with_cleanup
|
browser.close = _close_with_cleanup
|
||||||
|
|
||||||
@@ -281,7 +285,7 @@ def launch_persistent_context(
|
|||||||
|
|
||||||
binary_path = ensure_binary()
|
binary_path = ensure_binary()
|
||||||
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
||||||
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
|
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
|
"Launching persistent stealth Chromium (headless=%s, user_data_dir=%s)",
|
||||||
@@ -314,8 +318,10 @@ def launch_persistent_context(
|
|||||||
_original_close = context.close
|
_original_close = context.close
|
||||||
|
|
||||||
def _close_with_cleanup() -> None:
|
def _close_with_cleanup() -> None:
|
||||||
_original_close()
|
try:
|
||||||
pw.stop()
|
_original_close()
|
||||||
|
finally:
|
||||||
|
pw.stop()
|
||||||
|
|
||||||
context.close = _close_with_cleanup
|
context.close = _close_with_cleanup
|
||||||
|
|
||||||
@@ -394,7 +400,7 @@ async def launch_persistent_context_async(
|
|||||||
|
|
||||||
binary_path = ensure_binary()
|
binary_path = ensure_binary()
|
||||||
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
timezone, locale = _maybe_resolve_geoip(geoip, proxy, timezone, locale)
|
||||||
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale)
|
chrome_args = _build_args(stealth_args, args, timezone=timezone, locale=locale, headless=headless)
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
|
"Launching persistent stealth Chromium async (headless=%s, user_data_dir=%s)",
|
||||||
@@ -427,8 +433,10 @@ async def launch_persistent_context_async(
|
|||||||
_original_close = context.close
|
_original_close = context.close
|
||||||
|
|
||||||
async def _close_with_cleanup() -> None:
|
async def _close_with_cleanup() -> None:
|
||||||
await _original_close()
|
try:
|
||||||
await pw.stop()
|
await _original_close()
|
||||||
|
finally:
|
||||||
|
await pw.stop()
|
||||||
|
|
||||||
context.close = _close_with_cleanup
|
context.close = _close_with_cleanup
|
||||||
|
|
||||||
@@ -514,8 +522,10 @@ def launch_context(
|
|||||||
_original_ctx_close = context.close
|
_original_ctx_close = context.close
|
||||||
|
|
||||||
def _close_context_with_cleanup() -> None:
|
def _close_context_with_cleanup() -> None:
|
||||||
_original_ctx_close()
|
try:
|
||||||
browser.close()
|
_original_ctx_close()
|
||||||
|
finally:
|
||||||
|
browser.close()
|
||||||
|
|
||||||
context.close = _close_context_with_cleanup
|
context.close = _close_context_with_cleanup
|
||||||
|
|
||||||
@@ -609,6 +619,7 @@ def _build_args(
|
|||||||
extra_args: list[str] | None,
|
extra_args: list[str] | None,
|
||||||
timezone: str | None = None,
|
timezone: str | None = None,
|
||||||
locale: str | None = None,
|
locale: str | None = None,
|
||||||
|
headless: bool = True,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Combine stealth args with user-provided args and locale flags.
|
"""Combine stealth args with user-provided args and locale flags.
|
||||||
|
|
||||||
@@ -621,6 +632,16 @@ def _build_args(
|
|||||||
for arg in get_default_stealth_args():
|
for arg in get_default_stealth_args():
|
||||||
seen[arg.split("=", 1)[0]] = arg
|
seen[arg.split("=", 1)[0]] = arg
|
||||||
|
|
||||||
|
# GPU blocklist bypass:
|
||||||
|
# - Headed mode (all platforms): Chromium blocks WebGL on software GPUs
|
||||||
|
# in Docker/Xvfb. Flag lets SwiftShader serve WebGL. See issue #56.
|
||||||
|
# - Windows (all modes): Chromium's GPU blocklist blocks WebGPU for the
|
||||||
|
# Microsoft Basic Render Driver. Dawn's adapter_blocklist bypass alone
|
||||||
|
# isn't enough — need this flag too. Linux doesn't need it.
|
||||||
|
import platform as _platform
|
||||||
|
if not headless or _platform.system() == "Windows":
|
||||||
|
seen["--ignore-gpu-blocklist"] = "--ignore-gpu-blocklist"
|
||||||
|
|
||||||
if extra_args:
|
if extra_args:
|
||||||
for arg in extra_args:
|
for arg in extra_args:
|
||||||
key = arg.split("=", 1)[0]
|
key = arg.split("=", 1)[0]
|
||||||
|
|||||||
@@ -15,13 +15,14 @@ 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.6"
|
CHROMIUM_VERSION = "145.0.7632.159.8"
|
||||||
|
|
||||||
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
|
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
|
||||||
"linux-x64": "145.0.7632.159.6",
|
"linux-x64": "145.0.7632.159.8",
|
||||||
|
"linux-arm64": "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.159.7",
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import os
|
|||||||
import platform
|
import platform
|
||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
@@ -55,13 +56,14 @@ def _show_welcome() -> None:
|
|||||||
marker = get_cache_dir() / ".welcome_shown"
|
marker = get_cache_dir() / ".welcome_shown"
|
||||||
if marker.exists():
|
if marker.exists():
|
||||||
return
|
return
|
||||||
print()
|
sys.stderr.write("\n")
|
||||||
print(" CloakBrowser — stealth Chromium for automation")
|
sys.stderr.write(" CloakBrowser — stealth Chromium for automation\n")
|
||||||
print(" https://github.com/CloakHQ/CloakBrowser")
|
sys.stderr.write(" https://github.com/CloakHQ/CloakBrowser\n")
|
||||||
print()
|
sys.stderr.write("\n")
|
||||||
print(" Issues? https://github.com/CloakHQ/CloakBrowser/issues")
|
sys.stderr.write(" Issues? https://github.com/CloakHQ/CloakBrowser/issues\n")
|
||||||
print(" Star us if CloakBrowser helps your project!")
|
sys.stderr.write(" Donate? https://ko-fi.com/cloakhq\n")
|
||||||
print()
|
sys.stderr.write(" Star us if CloakBrowser helps your project!\n")
|
||||||
|
sys.stderr.write("\n")
|
||||||
try:
|
try:
|
||||||
marker.parent.mkdir(parents=True, exist_ok=True)
|
marker.parent.mkdir(parents=True, exist_ok=True)
|
||||||
marker.write_text("")
|
marker.write_text("")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# agent-browser + CloakBrowser: AI browser agent with stealth fingerprints.
|
||||||
|
#
|
||||||
|
# agent-browser is a Node.js CLI for browser automation with session management.
|
||||||
|
# CloakBrowser provides the stealth Chromium binary.
|
||||||
|
#
|
||||||
|
# Requires: npm install -g agent-browser
|
||||||
|
# pip install cloakbrowser (to auto-download the binary)
|
||||||
|
#
|
||||||
|
# Note: agent-browser launches Chrome itself via env vars — it can't connect
|
||||||
|
# to an existing browser via CDP. So we pass the binary path and stealth args directly.
|
||||||
|
|
||||||
|
# Get CloakBrowser binary path (auto-downloads if needed)
|
||||||
|
BINARY_PATH=$(python3 -c "from cloakbrowser.download import ensure_binary; print(ensure_binary())")
|
||||||
|
|
||||||
|
# Get stealth args from our wrapper (comma-separated for agent-browser)
|
||||||
|
STEALTH_ARGS=$(python3 -c "from cloakbrowser.config import get_default_stealth_args; print(','.join(get_default_stealth_args()))")
|
||||||
|
|
||||||
|
# Point agent-browser at CloakBrowser
|
||||||
|
export AGENT_BROWSER_EXECUTABLE_PATH="$BINARY_PATH"
|
||||||
|
export AGENT_BROWSER_ARGS="$STEALTH_ARGS"
|
||||||
|
|
||||||
|
# Open a page
|
||||||
|
agent-browser --session stealth-test open "https://example.com"
|
||||||
|
|
||||||
|
# Get page title
|
||||||
|
agent-browser --session stealth-test eval "document.title"
|
||||||
|
|
||||||
|
# Check stealth
|
||||||
|
agent-browser --session stealth-test eval "JSON.stringify({webdriver: navigator.webdriver, plugins: navigator.plugins.length, platform: navigator.platform})"
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
"""browser-use + CloakBrowser: AI agent with stealth fingerprints.
|
||||||
|
|
||||||
|
browser-use handles AI agent logic, CloakBrowser handles bot detection.
|
||||||
|
Your agent can now browse sites behind Cloudflare, reCAPTCHA, DataDome.
|
||||||
|
|
||||||
|
Requires: pip install browser-use cloakbrowser langchain-openai
|
||||||
|
Set OPENAI_API_KEY (or swap for another LLM provider).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from browser_use import Agent, Browser, BrowserConfig
|
||||||
|
from langchain_openai import ChatOpenAI
|
||||||
|
|
||||||
|
from cloakbrowser import launch_async
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
# Step 1: Launch CloakBrowser (handles binary, stealth args, fingerprints)
|
||||||
|
cb_browser = await launch_async(
|
||||||
|
headless=True,
|
||||||
|
args=["--remote-debugging-port=9242", "--remote-debugging-address=127.0.0.1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: Connect browser-use to the stealth browser via CDP
|
||||||
|
config = BrowserConfig(cdp_url="http://127.0.0.1:9242")
|
||||||
|
browser = Browser(config=config)
|
||||||
|
|
||||||
|
# Step 3: Run your AI agent — it browses through CloakBrowser
|
||||||
|
agent = Agent(
|
||||||
|
task="Go to https://www.google.com and search for 'browser automation'",
|
||||||
|
llm=ChatOpenAI(model="gpt-4o-mini"),
|
||||||
|
browser=browser,
|
||||||
|
)
|
||||||
|
|
||||||
|
result = await agent.run()
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
await cb_browser.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
"""Crawl4AI + CloakBrowser: LLM-ready web crawling with stealth fingerprints.
|
||||||
|
|
||||||
|
Crawl4AI handles extraction and markdown conversion,
|
||||||
|
CloakBrowser handles bot detection.
|
||||||
|
|
||||||
|
Requires: pip install crawl4ai cloakbrowser
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from crawl4ai import AsyncWebCrawler, BrowserConfig, CrawlerRunConfig
|
||||||
|
|
||||||
|
from cloakbrowser import launch_async
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
# Step 1: Launch CloakBrowser with remote debugging
|
||||||
|
cb_browser = await launch_async(
|
||||||
|
headless=True,
|
||||||
|
args=["--remote-debugging-port=9243", "--remote-debugging-address=127.0.0.1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Step 2: Connect Crawl4AI to the stealth browser via CDP
|
||||||
|
browser_config = BrowserConfig(cdp_url="http://127.0.0.1:9243")
|
||||||
|
run_config = CrawlerRunConfig()
|
||||||
|
|
||||||
|
async with AsyncWebCrawler(config=browser_config) as crawler:
|
||||||
|
result = await crawler.arun(
|
||||||
|
"https://example.com",
|
||||||
|
config=run_config,
|
||||||
|
)
|
||||||
|
print(f"Extracted {len(result.markdown)} chars of markdown")
|
||||||
|
print(result.markdown[:500])
|
||||||
|
|
||||||
|
await cb_browser.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
"""Crawlee + CloakBrowser: stealth web crawling with PlaywrightCrawler.
|
||||||
|
|
||||||
|
Uses a custom BrowserPlugin to swap Crawlee's default Chromium
|
||||||
|
for CloakBrowser's patched binary with source-level fingerprint patches.
|
||||||
|
|
||||||
|
Requires: pip install cloakbrowser "crawlee[playwright]"
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from cloakbrowser.config import IGNORE_DEFAULT_ARGS, get_default_stealth_args
|
||||||
|
from cloakbrowser.download import ensure_binary
|
||||||
|
from typing_extensions import override
|
||||||
|
|
||||||
|
from crawlee.browsers import (
|
||||||
|
BrowserPool,
|
||||||
|
PlaywrightBrowserController,
|
||||||
|
PlaywrightBrowserPlugin,
|
||||||
|
)
|
||||||
|
from crawlee.crawlers import PlaywrightCrawler, PlaywrightCrawlingContext
|
||||||
|
|
||||||
|
|
||||||
|
class CloakBrowserPlugin(PlaywrightBrowserPlugin):
|
||||||
|
"""Browser plugin that uses CloakBrowser's patched Chromium,
|
||||||
|
but otherwise keeps the functionality of PlaywrightBrowserPlugin.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@override
|
||||||
|
async def new_browser(self) -> PlaywrightBrowserController:
|
||||||
|
if not self._playwright:
|
||||||
|
raise RuntimeError('Playwright browser plugin is not initialized.')
|
||||||
|
|
||||||
|
binary_path = ensure_binary()
|
||||||
|
stealth_args = get_default_stealth_args()
|
||||||
|
|
||||||
|
# Merge CloakBrowser stealth args with any user-provided launch options.
|
||||||
|
launch_options = dict(self._browser_launch_options)
|
||||||
|
launch_options.pop('executable_path', None)
|
||||||
|
launch_options.pop('chromium_sandbox', None)
|
||||||
|
existing_args = list(launch_options.pop('args', []))
|
||||||
|
launch_options['args'] = [*existing_args, *stealth_args]
|
||||||
|
|
||||||
|
return PlaywrightBrowserController(
|
||||||
|
browser=await self._playwright.chromium.launch(
|
||||||
|
executable_path=binary_path,
|
||||||
|
ignore_default_args=IGNORE_DEFAULT_ARGS,
|
||||||
|
**launch_options,
|
||||||
|
),
|
||||||
|
max_open_pages_per_browser=1,
|
||||||
|
# CloakBrowser handles fingerprints at the binary level.
|
||||||
|
header_generator=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
crawler = PlaywrightCrawler(
|
||||||
|
max_requests_per_crawl=10,
|
||||||
|
browser_pool=BrowserPool(plugins=[CloakBrowserPlugin()]),
|
||||||
|
)
|
||||||
|
|
||||||
|
@crawler.router.default_handler
|
||||||
|
async def request_handler(context: PlaywrightCrawlingContext) -> None:
|
||||||
|
context.log.info(f'Processing {context.request.url} ...')
|
||||||
|
title = await context.page.title()
|
||||||
|
await context.push_data({'url': context.request.url, 'title': title})
|
||||||
|
await context.enqueue_links()
|
||||||
|
|
||||||
|
await crawler.run(['https://example.com'])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
"""LangChain + CloakBrowser: load web pages behind bot detection into LangChain Documents.
|
||||||
|
|
||||||
|
LangChain's PlaywrightURLLoader hardcodes chromium.launch() with no way to pass
|
||||||
|
a custom binary. This example uses CloakBrowser directly as a stealth document loader
|
||||||
|
that produces LangChain Document objects.
|
||||||
|
|
||||||
|
Requires: pip install langchain-core cloakbrowser
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from langchain_core.documents import Document
|
||||||
|
|
||||||
|
from cloakbrowser import launch_async
|
||||||
|
|
||||||
|
|
||||||
|
async def load_urls_stealth(urls: list[str], **launch_kwargs) -> list[Document]:
|
||||||
|
"""Load URLs using CloakBrowser stealth browser, return LangChain Documents."""
|
||||||
|
browser = await launch_async(headless=True, **launch_kwargs)
|
||||||
|
page = await browser.new_page()
|
||||||
|
docs = []
|
||||||
|
|
||||||
|
for url in urls:
|
||||||
|
await page.goto(url, wait_until="domcontentloaded")
|
||||||
|
text = await page.evaluate("document.body.innerText")
|
||||||
|
title = await page.title()
|
||||||
|
docs.append(Document(
|
||||||
|
page_content=text,
|
||||||
|
metadata={"source": url, "title": title},
|
||||||
|
))
|
||||||
|
|
||||||
|
await browser.close()
|
||||||
|
return docs
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
urls = [
|
||||||
|
"https://example.com",
|
||||||
|
"https://httpbin.org/html",
|
||||||
|
]
|
||||||
|
|
||||||
|
docs = await load_urls_stealth(urls)
|
||||||
|
|
||||||
|
for doc in docs:
|
||||||
|
print(f"--- {doc.metadata['title']} ({doc.metadata['source']}) ---")
|
||||||
|
print(doc.page_content[:300])
|
||||||
|
print()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""Scrapling + CloakBrowser: adaptive web scraping with stealth fingerprints.
|
||||||
|
|
||||||
|
Scrapling handles parsing and element tracking,
|
||||||
|
CloakBrowser handles bot detection.
|
||||||
|
|
||||||
|
Requires: pip install scrapling[all] cloakbrowser
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from urllib.request import urlopen
|
||||||
|
|
||||||
|
from scrapling.fetchers import StealthyFetcher
|
||||||
|
|
||||||
|
from cloakbrowser import launch_async
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
# Launch CloakBrowser with remote debugging
|
||||||
|
cb_browser = await launch_async(
|
||||||
|
headless=True,
|
||||||
|
args=["--remote-debugging-port=9245", "--remote-debugging-address=127.0.0.1"],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Get the WebSocket URL from Chrome (Scrapling requires ws:// scheme)
|
||||||
|
info = json.loads(urlopen("http://127.0.0.1:9245/json/version").read())
|
||||||
|
ws_url = info["webSocketDebuggerUrl"]
|
||||||
|
|
||||||
|
# Connect Scrapling to the stealth browser via CDP
|
||||||
|
page = await StealthyFetcher.async_fetch(
|
||||||
|
"https://example.com",
|
||||||
|
cdp_url=ws_url,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Title: {page.css('title::text').get()}")
|
||||||
|
print(f"Text: {page.css('p::text').getall()}")
|
||||||
|
|
||||||
|
await cb_browser.close()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
"""Selenium + CloakBrowser: use stealth Chromium with Selenium WebDriver.
|
||||||
|
|
||||||
|
CloakBrowser provides the binary and stealth args.
|
||||||
|
Selenium drives it via ChromeDriver.
|
||||||
|
|
||||||
|
Requires: pip install selenium cloakbrowser
|
||||||
|
Note: ChromeDriver version must match Chromium 145.
|
||||||
|
pip install chromedriver-autoinstaller or download manually.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from selenium import webdriver
|
||||||
|
from selenium.webdriver.chrome.options import Options
|
||||||
|
|
||||||
|
from cloakbrowser.config import get_default_stealth_args
|
||||||
|
from cloakbrowser.download import ensure_binary
|
||||||
|
|
||||||
|
binary_path = ensure_binary()
|
||||||
|
stealth_args = get_default_stealth_args()
|
||||||
|
|
||||||
|
options = Options()
|
||||||
|
options.binary_location = binary_path
|
||||||
|
options.add_argument("--headless")
|
||||||
|
for arg in stealth_args:
|
||||||
|
options.add_argument(arg)
|
||||||
|
|
||||||
|
driver = webdriver.Chrome(options=options)
|
||||||
|
|
||||||
|
driver.get("https://example.com")
|
||||||
|
print(f"Selenium + CloakBrowser: {driver.title}")
|
||||||
|
|
||||||
|
# Verify stealth
|
||||||
|
result = driver.execute_script("""
|
||||||
|
return {
|
||||||
|
webdriver: navigator.webdriver,
|
||||||
|
plugins: navigator.plugins.length,
|
||||||
|
platform: navigator.platform,
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
print(f"Stealth checks: {result}")
|
||||||
|
|
||||||
|
driver.quit()
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
"""undetected-chromedriver + CloakBrowser: double stealth layer.
|
||||||
|
|
||||||
|
undetected-chromedriver patches ChromeDriver detection signals,
|
||||||
|
CloakBrowser patches the browser fingerprints at the C++ level.
|
||||||
|
|
||||||
|
Requires: pip install undetected-chromedriver cloakbrowser
|
||||||
|
"""
|
||||||
|
|
||||||
|
import undetected_chromedriver as uc
|
||||||
|
|
||||||
|
from cloakbrowser.config import get_chromium_version, get_default_stealth_args
|
||||||
|
from cloakbrowser.download import ensure_binary
|
||||||
|
|
||||||
|
binary_path = ensure_binary()
|
||||||
|
stealth_args = get_default_stealth_args()
|
||||||
|
chromium_major = int(get_chromium_version().split(".")[0])
|
||||||
|
|
||||||
|
options = uc.ChromeOptions()
|
||||||
|
options.binary_location = binary_path
|
||||||
|
options.add_argument("--headless")
|
||||||
|
for arg in stealth_args:
|
||||||
|
options.add_argument(arg)
|
||||||
|
|
||||||
|
driver = uc.Chrome(options=options, version_main=chromium_major)
|
||||||
|
|
||||||
|
driver.get("https://example.com")
|
||||||
|
print(f"undetected-chromedriver + CloakBrowser: {driver.title}")
|
||||||
|
|
||||||
|
# Verify stealth
|
||||||
|
result = driver.execute_script("""
|
||||||
|
return {
|
||||||
|
webdriver: navigator.webdriver,
|
||||||
|
plugins: navigator.plugins.length,
|
||||||
|
platform: navigator.platform,
|
||||||
|
hardwareConcurrency: navigator.hardwareConcurrency,
|
||||||
|
}
|
||||||
|
""")
|
||||||
|
print(f"Stealth checks: {result}")
|
||||||
|
|
||||||
|
driver.quit()
|
||||||
+6
-5
@@ -11,12 +11,12 @@
|
|||||||
|
|
||||||
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
|
||||||
- **Free and open source** — no subscriptions, no usage limits
|
- **Free and open source** — no subscriptions, no usage limits
|
||||||
- **Works with any framework** — also tested with Selenium, undetected-chromedriver, browser-use, Crawl4AI, and agent-browser
|
- **Works with any framework** — tested with browser-use, Crawl4AI, Scrapling, Stagehand ([example](examples/stagehand.ts)), LangChain, Selenium, and more
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
@@ -203,10 +203,11 @@ const page = await browser.newPage();
|
|||||||
|
|
||||||
| Platform | Chromium | Patches | Status |
|
| Platform | Chromium | Patches | Status |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Linux x86_64 | 145 | 26 | ✅ Latest |
|
| Linux x86_64 | 145 | 33 | ✅ Latest |
|
||||||
|
| Linux arm64 (RPi, Graviton) | 145 | 33 | ✅ Latest |
|
||||||
| macOS arm64 (Apple Silicon) | 145 | 26 | ✅ Latest |
|
| macOS arm64 (Apple Silicon) | 145 | 26 | ✅ Latest |
|
||||||
| macOS x86_64 (Intel) | 145 | 26 | ✅ Latest |
|
| macOS x86_64 (Intel) | 145 | 26 | ✅ Latest |
|
||||||
| Windows x86_64 | 145 | 26 | ✅ Latest |
|
| Windows x86_64 | 145 | 33 | ✅ Latest |
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -264,7 +265,7 @@ export CLOAKBROWSER_BINARY_PATH=~/.cloakbrowser/chromium-145.0.7632.159.2/chrome
|
|||||||
export CLOAKBROWSER_BINARY_PATH=~/.cloakbrowser/chromium-145.0.7632.109.2/Chromium.app/Contents/MacOS/Chromium
|
export CLOAKBROWSER_BINARY_PATH=~/.cloakbrowser/chromium-145.0.7632.109.2/Chromium.app/Contents/MacOS/Chromium
|
||||||
|
|
||||||
# Windows
|
# Windows
|
||||||
set CLOAKBROWSER_BINARY_PATH=%USERPROFILE%\.cloakbrowser\chromium-145.0.7632.109.2\chrome.exe
|
set CLOAKBROWSER_BINARY_PATH=%USERPROFILE%\.cloakbrowser\chromium-145.0.7632.159.7\chrome.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* Stagehand + CloakBrowser: AI browser automation with stealth fingerprints.
|
||||||
|
*
|
||||||
|
* Stagehand handles AI-powered navigation and actions,
|
||||||
|
* CloakBrowser handles bot detection.
|
||||||
|
*
|
||||||
|
* Requires: npm install @browserbasehq/stagehand cloakbrowser
|
||||||
|
* Set OPENAI_API_KEY for the AI model.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* CLOAKBROWSER_BINARY_PATH=/path/to/chrome npx tsx examples/stagehand.ts
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Stagehand } from "@browserbasehq/stagehand";
|
||||||
|
import { ensureBinary } from "../src/download.js";
|
||||||
|
import { getDefaultStealthArgs } from "../src/config.js";
|
||||||
|
|
||||||
|
const binaryPath = await ensureBinary();
|
||||||
|
const stealthArgs = getDefaultStealthArgs();
|
||||||
|
|
||||||
|
const stagehand = new Stagehand({
|
||||||
|
env: "LOCAL",
|
||||||
|
localBrowserLaunchOptions: {
|
||||||
|
executablePath: binaryPath,
|
||||||
|
args: stealthArgs,
|
||||||
|
headless: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await stagehand.init();
|
||||||
|
|
||||||
|
const page = stagehand.context.pages()[0];
|
||||||
|
await page.goto("https://example.com");
|
||||||
|
console.log(`Stagehand + CloakBrowser: ${await page.title()}`);
|
||||||
|
|
||||||
|
await stagehand.close();
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloakbrowser",
|
"name": "cloakbrowser",
|
||||||
"version": "0.3.14",
|
"version": "0.3.19",
|
||||||
"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",
|
||||||
|
|||||||
@@ -20,6 +20,15 @@ export function buildArgs(options: LaunchOptions): string[] {
|
|||||||
seen.set(arg.split("=")[0], arg);
|
seen.set(arg.split("=")[0], arg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// GPU blocklist bypass:
|
||||||
|
// - Headed mode (all platforms): Chromium blocks WebGL on software GPUs
|
||||||
|
// in Docker/Xvfb. Flag lets SwiftShader serve WebGL. See issue #56.
|
||||||
|
// - Windows (all modes): Chromium's GPU blocklist blocks WebGPU for the
|
||||||
|
// Microsoft Basic Render Driver. Dawn's adapter_blocklist bypass alone
|
||||||
|
// isn't enough. Linux doesn't need it.
|
||||||
|
if (options.headless === false || process.platform === "win32") {
|
||||||
|
seen.set("--ignore-gpu-blocklist", "--ignore-gpu-blocklist");
|
||||||
|
}
|
||||||
if (options.args) {
|
if (options.args) {
|
||||||
for (const arg of options.args) {
|
for (const arg of options.args) {
|
||||||
const key = arg.split("=")[0];
|
const key = arg.split("=")[0];
|
||||||
|
|||||||
+4
-3
@@ -27,13 +27,14 @@ 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.6";
|
export const CHROMIUM_VERSION = "145.0.7632.159.8";
|
||||||
|
|
||||||
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
|
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
|
||||||
"linux-x64": "145.0.7632.159.6",
|
"linux-x64": "145.0.7632.159.8",
|
||||||
|
"linux-arm64": "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.159.7",
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
+10
-8
@@ -146,13 +146,14 @@ export async function checkForUpdate(): Promise<string | null> {
|
|||||||
function showWelcome(): void {
|
function showWelcome(): void {
|
||||||
const marker = path.join(getCacheDir(), ".welcome_shown");
|
const marker = path.join(getCacheDir(), ".welcome_shown");
|
||||||
if (fs.existsSync(marker)) return;
|
if (fs.existsSync(marker)) return;
|
||||||
console.log();
|
console.error();
|
||||||
console.log(" CloakBrowser — stealth Chromium for automation");
|
console.error(" CloakBrowser — stealth Chromium for automation");
|
||||||
console.log(" https://github.com/CloakHQ/CloakBrowser");
|
console.error(" https://github.com/CloakHQ/CloakBrowser");
|
||||||
console.log();
|
console.error();
|
||||||
console.log(" Issues? https://github.com/CloakHQ/CloakBrowser/issues");
|
console.error(" Issues? https://github.com/CloakHQ/CloakBrowser/issues");
|
||||||
console.log(" Star us if CloakBrowser helps your project!");
|
console.error(" Donate? https://ko-fi.com/cloakhq");
|
||||||
console.log();
|
console.error(" Star us if CloakBrowser helps your project!");
|
||||||
|
console.error();
|
||||||
try {
|
try {
|
||||||
fs.mkdirSync(getCacheDir(), { recursive: true });
|
fs.mkdirSync(getCacheDir(), { recursive: true });
|
||||||
fs.writeFileSync(marker, "");
|
fs.writeFileSync(marker, "");
|
||||||
@@ -227,7 +228,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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