mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8e09656aa | ||
|
|
724d49f65b | ||
|
|
ed79560e5f | ||
|
|
829e4b881f | ||
|
|
3880d30d0f | ||
|
|
9c533e4120 | ||
|
|
98c216f07e | ||
|
|
ee953709b0 | ||
|
|
a45fdc4d7e |
@@ -0,0 +1,34 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
python:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pip install -e ".[dev]" pytest pytest-asyncio
|
||||||
|
- name: Run tests
|
||||||
|
run: pytest tests/ -v -m "not slow"
|
||||||
|
|
||||||
|
javascript:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
- name: Install and build
|
||||||
|
run: cd js && npm install && npm run build
|
||||||
|
- name: Typecheck
|
||||||
|
run: cd js && npm run typecheck
|
||||||
|
- name: Run tests
|
||||||
|
run: cd js && npm test
|
||||||
@@ -6,6 +6,22 @@ Changes are tagged: **[wrapper]** for Python/JS wrapper, **[binary]** for Chromi
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## [0.3.10] — 2026-03-07
|
||||||
|
|
||||||
|
- **[binary]** Upgrade Linux build to 145.0.7632.159.2
|
||||||
|
- **[binary]** Fix detection regression caused by unnecessary browser flag (fixes #16)
|
||||||
|
- **[binary]** Fix fingerprint consistency in offline audio rendering
|
||||||
|
- **[wrapper]** Add `cloakserve` CDP server mode for Docker — exposes Chrome DevTools Protocol on `0.0.0.0:9222` for external tool integration
|
||||||
|
- **[wrapper]** Add wrapper regression tests: page.goto timing with stealth init (#9), add_init_script compatibility with proxy auth (#27)
|
||||||
|
|
||||||
|
## [0.3.9] — 2026-03-05
|
||||||
|
|
||||||
|
- **[binary]** Upgrade Chromium base to 145.0.7632.159 (Linux x64). macOS and Windows remain on 145.0.7632.109.2
|
||||||
|
- **[binary]** WebGPU adapter spoofing for headless/Docker, timezone multi-context fix, stealth audit phase 2 (6 detection vector fixes), font auto-hide for cross-platform fingerprints
|
||||||
|
- **[wrapper]** Default Playwright backend switched from `patchright` to stock `playwright`. Patchright broke proxy auth and `add_init_script` (#27) and is redundant since the binary handles stealth at C++ level. Opt in with `launch(backend="patchright")` or `CLOAKBROWSER_BACKEND=patchright` env var. Install: `pip install cloakbrowser[patchright]`
|
||||||
|
- **[wrapper]** Deduplicate CLI flags when user args overlap with stealth defaults — user values win cleanly instead of passing both to Chromium
|
||||||
|
- **[wrapper]** Extract shared `buildArgs` into `js/src/args.ts` (JS DRY fix), guard debug logging behind `DEBUG=cloakbrowser` env var
|
||||||
|
|
||||||
## [0.3.7] — 2026-03-05
|
## [0.3.7] — 2026-03-05
|
||||||
|
|
||||||
- **[wrapper]** Unify timezone parameter: rename `timezone_id` to `timezone` in `launch_context()`, `launch_persistent_context()`, and `launch_persistent_context_async()` (Python). Old `timezone_id` still works with a deprecation warning. JS: deprecate `timezoneId` on `LaunchContextOptions` — use `timezone` (inherited from `LaunchOptions`)
|
- **[wrapper]** Unify timezone parameter: rename `timezone_id` to `timezone` in `launch_context()`, `launch_persistent_context()`, and `launch_persistent_context_async()` (Python). Old `timezone_id` still works with a deprecation warning. JS: deprecate `timezoneId` on `LaunchContextOptions` — use `timezone` (inherited from `LaunchOptions`)
|
||||||
|
|||||||
+5
-2
@@ -10,7 +10,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libglib2.0-0 libgtk-3-0 libpangocairo-1.0-0 libcairo-gobject2 \
|
libglib2.0-0 libgtk-3-0 libpangocairo-1.0-0 libcairo-gobject2 \
|
||||||
libgdk-pixbuf-2.0-0 libxss1 libxtst6 fonts-liberation \
|
libgdk-pixbuf-2.0-0 libxss1 libxtst6 fonts-liberation \
|
||||||
xvfb xdotool \
|
xvfb xdotool \
|
||||||
curl ca-certificates \
|
curl ca-certificates socat \
|
||||||
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
||||||
&& apt-get install -y --no-install-recommends nodejs \
|
&& apt-get install -y --no-install-recommends nodejs \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
@@ -36,7 +36,10 @@ RUN python -c "from cloakbrowser import ensure_binary; ensure_binary()" \
|
|||||||
|
|
||||||
# CLI shortcuts
|
# CLI shortcuts
|
||||||
COPY bin/cloaktest /usr/local/bin/cloaktest
|
COPY bin/cloaktest /usr/local/bin/cloaktest
|
||||||
RUN chmod +x /usr/local/bin/cloaktest
|
COPY bin/cloakserve /usr/local/bin/cloakserve
|
||||||
|
RUN chmod +x /usr/local/bin/cloaktest /usr/local/bin/cloakserve
|
||||||
|
|
||||||
|
EXPOSE 9222
|
||||||
|
|
||||||
# Xvfb entrypoint for headed mode support
|
# Xvfb entrypoint for headed mode support
|
||||||
COPY bin/docker-entrypoint.sh /entrypoint.sh
|
COPY bin/docker-entrypoint.sh /entrypoint.sh
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
<a href="https://github.com/CloakHQ/CloakBrowser"><img src="https://img.shields.io/github/last-commit/cloakhq/cloakbrowser" alt="Last Commit"></a>
|
<a href="https://github.com/CloakHQ/CloakBrowser"><img src="https://img.shields.io/github/last-commit/cloakhq/cloakbrowser" alt="Last Commit"></a>
|
||||||
<br>
|
<br>
|
||||||
<a href="https://github.com/CloakHQ/CloakBrowser"><img src="https://img.shields.io/github/stars/cloakhq/cloakbrowser" alt="Stars"></a>
|
<a href="https://github.com/CloakHQ/CloakBrowser"><img src="https://img.shields.io/github/stars/cloakhq/cloakbrowser" alt="Stars"></a>
|
||||||
<a href="https://pepy.tech/projects/cloakbrowser"><img src="https://img.shields.io/pepy/dt/cloakbrowser?label=pypi&logo=pypi&logoColor=white" alt="PyPI Downloads"></a>
|
<a href="https://pypi.org/project/cloakbrowser/"><img src="https://img.shields.io/pepy/dt/cloakbrowser?label=pypi&logo=pypi&logoColor=white" alt="PyPI Downloads"></a>
|
||||||
<a href="https://www.npmjs.com/package/cloakbrowser"><img src="https://img.shields.io/npm/dt/cloakbrowser?label=npm&logo=npm&logoColor=white" alt="npm Downloads"></a>
|
<a href="https://www.npmjs.com/package/cloakbrowser"><img src="https://img.shields.io/npm/dt/cloakbrowser?label=npm&logo=npm&logoColor=white" alt="npm Downloads"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@@ -108,7 +108,7 @@ 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.5 (Chromium 145.0.7632.109)
|
## Latest: v0.3.10 (Chromium 145.0.7632.159.2)
|
||||||
|
|
||||||
- **All 4 platforms** — Linux x64, macOS arm64, macOS x64, and Windows x64 all on Chromium 145
|
- **All 4 platforms** — Linux x64, macOS arm64, macOS x64, and Windows x64 all on Chromium 145
|
||||||
- **26 fingerprint patches** — 10 new patches since v142 (screen, device memory, audio, WebGL, auto-spoof, and more)
|
- **26 fingerprint patches** — 10 new patches since v142 (screen, device memory, audio, WebGL, auto-spoof, and more)
|
||||||
@@ -125,7 +125,7 @@ See the full [CHANGELOG.md](CHANGELOG.md) for details.
|
|||||||
|
|
||||||
- **Config-level patches break** — `playwright-stealth`, `undetected-chromedriver`, and `puppeteer-extra` inject JavaScript or tweak flags. Every Chrome update breaks them. Antibot systems detect the patches themselves.
|
- **Config-level patches break** — `playwright-stealth`, `undetected-chromedriver`, and `puppeteer-extra` inject JavaScript or tweak flags. Every Chrome update breaks them. Antibot systems detect the patches themselves.
|
||||||
- **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.
|
||||||
- **Two layers of stealth** — C++ patches handle fingerprints (GPU, screen, UA, hardware reporting), while the Patchright driver defers Playwright's binding registration and randomizes internal world names. Most stealth tools only do one or the other.
|
- **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 any browser automation framework** — tested and passing stealth checks with Playwright, Puppeteer, Selenium, undetected-chromedriver, browser-use, Crawl4AI, and agent-browser. Just point any Chromium-based framework at the binary path.
|
- **Works with any browser automation framework** — tested and passing stealth checks with Playwright, Puppeteer, Selenium, undetected-chromedriver, browser-use, Crawl4AI, and agent-browser. Just point any Chromium-based framework at the binary path.
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ from cloakbrowser import binary_info, clear_cache, ensure_binary
|
|||||||
|
|
||||||
# Check binary installation status
|
# Check binary installation status
|
||||||
print(binary_info())
|
print(binary_info())
|
||||||
# {'version': '145.0.7632.109', 'platform': 'linux-x64', 'installed': True, ...}
|
# {'version': '145.0.7632.159.2', 'platform': 'linux-x64', 'installed': True, ...}
|
||||||
|
|
||||||
# Force re-download
|
# Force re-download
|
||||||
clear_cache()
|
clear_cache()
|
||||||
@@ -506,13 +506,18 @@ The wrapper auto-downloads the correct binary for your platform.
|
|||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
Pre-built image on Docker Hub — no install, no setup:
|
Pre-built image on Docker Hub — no install, no setup.
|
||||||
|
|
||||||
|
### Quick test
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run the stealth test suite
|
|
||||||
docker run --rm cloakhq/cloakbrowser cloaktest
|
docker run --rm cloakhq/cloakbrowser cloaktest
|
||||||
|
```
|
||||||
|
|
||||||
# Run your own script
|
### Run a script
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Inline script
|
||||||
docker run --rm cloakhq/cloakbrowser python -c "
|
docker run --rm cloakhq/cloakbrowser python -c "
|
||||||
from cloakbrowser import launch
|
from cloakbrowser import launch
|
||||||
browser = launch()
|
browser = launch()
|
||||||
@@ -522,6 +527,9 @@ print(page.title())
|
|||||||
browser.close()
|
browser.close()
|
||||||
"
|
"
|
||||||
|
|
||||||
|
# Mount your own script
|
||||||
|
docker run --rm -v ./my_script.py:/app/my_script.py cloakhq/cloakbrowser python my_script.py
|
||||||
|
|
||||||
# With a proxy
|
# With a proxy
|
||||||
docker run --rm cloakhq/cloakbrowser python -c "
|
docker run --rm cloakhq/cloakbrowser python -c "
|
||||||
from cloakbrowser import launch
|
from cloakbrowser import launch
|
||||||
@@ -533,7 +541,86 @@ browser.close()
|
|||||||
"
|
"
|
||||||
```
|
```
|
||||||
|
|
||||||
To extend with your own script:
|
### CDP server mode
|
||||||
|
|
||||||
|
Start a persistent stealth browser and connect to it remotely via Chrome DevTools Protocol:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d --name cloak -p 127.0.0.1:9222:9222 cloakhq/cloakbrowser cloakserve
|
||||||
|
```
|
||||||
|
|
||||||
|
Then connect from your host machine:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
pw = sync_playwright().start()
|
||||||
|
browser = pw.chromium.connect_over_cdp("http://localhost:9222")
|
||||||
|
page = browser.new_page()
|
||||||
|
page.goto("https://example.com")
|
||||||
|
print(page.title())
|
||||||
|
browser.close()
|
||||||
|
```
|
||||||
|
|
||||||
|
Pass extra flags to the browser:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# With proxy
|
||||||
|
docker run -d --name cloak -p 127.0.0.1:9222:9222 cloakhq/cloakbrowser \
|
||||||
|
cloakserve --proxy-server=http://proxy:8080
|
||||||
|
|
||||||
|
# Headed mode (renders to Xvfb inside container)
|
||||||
|
docker run -d --name cloak -p 127.0.0.1:9222:9222 cloakhq/cloakbrowser \
|
||||||
|
cloakserve --headless=false
|
||||||
|
```
|
||||||
|
|
||||||
|
Stop the server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker stop cloak && docker rm cloak
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Security:** CDP gives full control over the browser (execute JS, read pages, access files).
|
||||||
|
> The examples bind to `127.0.0.1` so only your machine can connect. Never expose port 9222
|
||||||
|
> to the public internet without additional authentication.
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
cloakbrowser:
|
||||||
|
image: cloakhq/cloakbrowser
|
||||||
|
command: cloakserve
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:9222:9222"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:9222/json/version"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 10s
|
||||||
|
```
|
||||||
|
|
||||||
|
Run multiple instances with different fingerprint seeds on different ports — each gets unique canvas noise, client rects, and other browser signals. Pass `--fingerprint=<seed>` in the command (e.g., `cloakserve --fingerprint=12345`).
|
||||||
|
|
||||||
|
**Persistent profiles** — mount a volume to keep cookies and sessions across container restarts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm -v ./my-profile:/profile cloakhq/cloakbrowser python -c "
|
||||||
|
from cloakbrowser import launch_persistent_context
|
||||||
|
ctx = launch_persistent_context('/profile')
|
||||||
|
page = ctx.new_page()
|
||||||
|
page.goto('https://example.com')
|
||||||
|
ctx.close()
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run again with the same volume — cookies, localStorage, and cache are restored automatically.
|
||||||
|
|
||||||
|
**Resource usage:** ~190MB RAM idle, ~280MB with 3 tabs. ~30MB per additional tab.
|
||||||
|
|
||||||
|
### Extend with your own image
|
||||||
|
|
||||||
```dockerfile
|
```dockerfile
|
||||||
FROM cloakhq/cloakbrowser
|
FROM cloakhq/cloakbrowser
|
||||||
@@ -630,13 +717,13 @@ export CLOAKBROWSER_BINARY_PATH=/path/to/your/chrome
|
|||||||
When auto-update downloads a newer binary, the previous version stays in `~/.cloakbrowser/`. Point `CLOAKBROWSER_BINARY_PATH` to the older cached binary:
|
When auto-update downloads a newer binary, the previous version stays in `~/.cloakbrowser/`. Point `CLOAKBROWSER_BINARY_PATH` to the older cached binary:
|
||||||
```bash
|
```bash
|
||||||
# Linux
|
# Linux
|
||||||
export CLOAKBROWSER_BINARY_PATH=~/.cloakbrowser/chromium-145.0.7632.109/chrome
|
export CLOAKBROWSER_BINARY_PATH=~/.cloakbrowser/chromium-145.0.7632.159.2/chrome
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
export CLOAKBROWSER_BINARY_PATH=~/.cloakbrowser/chromium-145.0.7632.109/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\chrome.exe
|
set CLOAKBROWSER_BINARY_PATH=%USERPROFILE%\.cloakbrowser\chromium-145.0.7632.109.2\chrome.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
**macOS: "App is damaged" or Gatekeeper blocks launch**
|
**macOS: "App is damaged" or Gatekeeper blocks launch**
|
||||||
@@ -648,7 +735,7 @@ 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
|
||||||
patchright 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**
|
||||||
@@ -699,6 +786,7 @@ await new Promise(r => setTimeout(r, 3000));
|
|||||||
```
|
```
|
||||||
|
|
||||||
Other tips for maximizing reCAPTCHA scores:
|
Other tips for maximizing reCAPTCHA scores:
|
||||||
|
- **Try the Patchright backend** — suppresses CDP automation signals that reCAPTCHA Enterprise detects. Install with `pip install cloakbrowser[patchright]`, then use `launch(backend="patchright")` or set `CLOAKBROWSER_BACKEND=patchright` globally. Note: Patchright breaks proxy auth and `add_init_script` — only use it when you need the extra CDP stealth
|
||||||
- **Use Playwright, not Puppeteer** — Puppeteer sends more CDP protocol traffic that reCAPTCHA detects ([details](#puppeteer))
|
- **Use Playwright, not Puppeteer** — Puppeteer sends more CDP protocol traffic that reCAPTCHA detects ([details](#puppeteer))
|
||||||
- **Use residential proxies** — datacenter IPs are flagged by IP reputation, not browser fingerprint
|
- **Use residential proxies** — datacenter IPs are flagged by IP reputation, not browser fingerprint
|
||||||
- **Spend 15+ seconds on the page** before triggering reCAPTCHA — short visits score lower
|
- **Spend 15+ seconds on the page** before triggering reCAPTCHA — short visits score lower
|
||||||
|
|||||||
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Launch stealth Chromium as a CDP server for remote connections.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
cloakserve # headless on port 9222
|
||||||
|
cloakserve --headless=false # headed (uses Xvfb in Docker)
|
||||||
|
cloakserve --proxy-server=host:port # with proxy
|
||||||
|
|
||||||
|
Connect from host:
|
||||||
|
playwright.chromium.connect_over_cdp("http://localhost:9222")
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
from cloakbrowser.config import get_default_stealth_args
|
||||||
|
from cloakbrowser.download import ensure_binary
|
||||||
|
|
||||||
|
EXPOSE_PORT = 9222 # External, 0.0.0.0 (user connects here)
|
||||||
|
CHROME_PORT = 9223 # Internal, 127.0.0.1 (Chrome binds here)
|
||||||
|
|
||||||
|
binary = ensure_binary()
|
||||||
|
|
||||||
|
chrome_args = [
|
||||||
|
binary,
|
||||||
|
f"--remote-debugging-port={CHROME_PORT}",
|
||||||
|
# "--remote-debugging-address=0.0.0.0", # TODO: enable after binary patch 032
|
||||||
|
# Sane defaults for running Chrome directly (outside Playwright)
|
||||||
|
"--no-first-run",
|
||||||
|
"--no-default-browser-check",
|
||||||
|
"--disable-dev-shm-usage",
|
||||||
|
"--disable-extensions",
|
||||||
|
"--disable-popup-blocking",
|
||||||
|
"--disable-background-networking",
|
||||||
|
"--metrics-recording-only",
|
||||||
|
] + get_default_stealth_args() + sys.argv[1:]
|
||||||
|
|
||||||
|
# Launch Chrome
|
||||||
|
chrome = subprocess.Popen(chrome_args)
|
||||||
|
|
||||||
|
# Wait for Chrome to start listening
|
||||||
|
time.sleep(2)
|
||||||
|
|
||||||
|
# Forward 0.0.0.0:9222 -> 127.0.0.1:9223
|
||||||
|
# Chrome hardcodes 127.0.0.1 for security; socat exposes it to Docker network.
|
||||||
|
# TODO: Remove socat after patching binary to support --remote-debugging-address
|
||||||
|
socat = subprocess.Popen([
|
||||||
|
"socat",
|
||||||
|
f"TCP-LISTEN:{EXPOSE_PORT},fork,reuseaddr,bind=0.0.0.0",
|
||||||
|
f"TCP:127.0.0.1:{CHROME_PORT}",
|
||||||
|
])
|
||||||
|
|
||||||
|
print(f"CloakBrowser CDP server ready on port {EXPOSE_PORT}", flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def cleanup(sig, frame):
|
||||||
|
chrome.terminate()
|
||||||
|
socat.terminate()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
signal.signal(signal.SIGTERM, cleanup)
|
||||||
|
signal.signal(signal.SIGINT, cleanup)
|
||||||
|
|
||||||
|
# Wait for Chrome to exit, then clean up socat
|
||||||
|
chrome.wait()
|
||||||
|
socat.terminate()
|
||||||
@@ -1 +1 @@
|
|||||||
__version__ = "0.3.8"
|
__version__ = "0.3.10"
|
||||||
|
|||||||
+87
-13
@@ -57,6 +57,7 @@ def launch(
|
|||||||
timezone: str | None = None,
|
timezone: str | None = None,
|
||||||
locale: str | None = None,
|
locale: str | None = None,
|
||||||
geoip: bool = False,
|
geoip: bool = False,
|
||||||
|
backend: str | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Launch stealth Chromium browser. Returns a Playwright Browser object.
|
"""Launch stealth Chromium browser. Returns a Playwright Browser object.
|
||||||
@@ -76,6 +77,10 @@ def launch(
|
|||||||
Requires ``pip install cloakbrowser[geoip]``. Downloads ~70 MB
|
Requires ``pip install cloakbrowser[geoip]``. Downloads ~70 MB
|
||||||
GeoLite2-City database on first use. Explicit timezone/locale
|
GeoLite2-City database on first use. Explicit timezone/locale
|
||||||
always override geoip results.
|
always override geoip results.
|
||||||
|
backend: Playwright backend — 'playwright' (default) or 'patchright'.
|
||||||
|
Patchright suppresses CDP signals (helps reCAPTCHA v3 Enterprise)
|
||||||
|
but breaks proxy auth and add_init_script.
|
||||||
|
Override globally with CLOAKBROWSER_BACKEND env var.
|
||||||
**kwargs: Passed directly to playwright.chromium.launch().
|
**kwargs: Passed directly to playwright.chromium.launch().
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -89,7 +94,7 @@ def launch(
|
|||||||
>>> print(page.title())
|
>>> print(page.title())
|
||||||
>>> browser.close()
|
>>> browser.close()
|
||||||
"""
|
"""
|
||||||
from patchright.sync_api import sync_playwright
|
sync_playwright = _import_sync_playwright(_resolve_backend(backend))
|
||||||
|
|
||||||
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)
|
||||||
@@ -127,6 +132,7 @@ async def launch_async(
|
|||||||
timezone: str | None = None,
|
timezone: str | None = None,
|
||||||
locale: str | None = None,
|
locale: str | None = None,
|
||||||
geoip: bool = False,
|
geoip: bool = False,
|
||||||
|
backend: str | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Async version of launch(). Returns a Playwright Browser object.
|
"""Async version of launch(). Returns a Playwright Browser object.
|
||||||
@@ -139,6 +145,7 @@ async def launch_async(
|
|||||||
timezone: IANA timezone (e.g. 'America/New_York'). Sets --fingerprint-timezone binary flag.
|
timezone: IANA timezone (e.g. 'America/New_York'). Sets --fingerprint-timezone binary flag.
|
||||||
locale: BCP 47 locale (e.g. 'en-US'). Sets --lang binary flag.
|
locale: BCP 47 locale (e.g. 'en-US'). Sets --lang binary flag.
|
||||||
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
||||||
|
backend: Playwright backend — 'playwright' (default) or 'patchright'.
|
||||||
**kwargs: Passed directly to playwright.chromium.launch().
|
**kwargs: Passed directly to playwright.chromium.launch().
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -157,7 +164,7 @@ async def launch_async(
|
|||||||
>>>
|
>>>
|
||||||
>>> asyncio.run(main())
|
>>> asyncio.run(main())
|
||||||
"""
|
"""
|
||||||
from patchright.async_api import async_playwright
|
async_playwright = _import_async_playwright(_resolve_backend(backend))
|
||||||
|
|
||||||
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)
|
||||||
@@ -199,6 +206,7 @@ def launch_persistent_context(
|
|||||||
timezone: str | None = None,
|
timezone: str | None = None,
|
||||||
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
|
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
|
||||||
geoip: bool = False,
|
geoip: bool = False,
|
||||||
|
backend: str | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Launch stealth browser with a persistent profile and return a BrowserContext.
|
"""Launch stealth browser with a persistent profile and return a BrowserContext.
|
||||||
@@ -223,6 +231,7 @@ def launch_persistent_context(
|
|||||||
Default: None (uses Chromium default, which is 'light').
|
Default: None (uses Chromium default, which is 'light').
|
||||||
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
||||||
Requires ``pip install cloakbrowser[geoip]``.
|
Requires ``pip install cloakbrowser[geoip]``.
|
||||||
|
backend: Playwright backend — 'playwright' (default) or 'patchright'.
|
||||||
**kwargs: Passed directly to playwright.chromium.launch_persistent_context().
|
**kwargs: Passed directly to playwright.chromium.launch_persistent_context().
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -236,7 +245,7 @@ def launch_persistent_context(
|
|||||||
>>> page.goto("https://protected-site.com")
|
>>> page.goto("https://protected-site.com")
|
||||||
>>> ctx.close() # Profile is saved; re-use path next run to restore state.
|
>>> ctx.close() # Profile is saved; re-use path next run to restore state.
|
||||||
"""
|
"""
|
||||||
from patchright.sync_api import sync_playwright
|
sync_playwright = _import_sync_playwright(_resolve_backend(backend))
|
||||||
|
|
||||||
timezone = _migrate_timezone_id(timezone, kwargs)
|
timezone = _migrate_timezone_id(timezone, kwargs)
|
||||||
|
|
||||||
@@ -297,6 +306,7 @@ async def launch_persistent_context_async(
|
|||||||
timezone: str | None = None,
|
timezone: str | None = None,
|
||||||
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
|
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
|
||||||
geoip: bool = False,
|
geoip: bool = False,
|
||||||
|
backend: str | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Async version of launch_persistent_context().
|
"""Async version of launch_persistent_context().
|
||||||
@@ -318,6 +328,7 @@ async def launch_persistent_context_async(
|
|||||||
timezone: IANA timezone (e.g. 'America/New_York').
|
timezone: IANA timezone (e.g. 'America/New_York').
|
||||||
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
|
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
|
||||||
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
||||||
|
backend: Playwright backend — 'playwright' (default) or 'patchright'.
|
||||||
**kwargs: Passed directly to playwright.chromium.launch_persistent_context().
|
**kwargs: Passed directly to playwright.chromium.launch_persistent_context().
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -336,7 +347,7 @@ async def launch_persistent_context_async(
|
|||||||
>>>
|
>>>
|
||||||
>>> asyncio.run(main())
|
>>> asyncio.run(main())
|
||||||
"""
|
"""
|
||||||
from patchright.async_api import async_playwright
|
async_playwright = _import_async_playwright(_resolve_backend(backend))
|
||||||
|
|
||||||
timezone = _migrate_timezone_id(timezone, kwargs)
|
timezone = _migrate_timezone_id(timezone, kwargs)
|
||||||
|
|
||||||
@@ -396,6 +407,7 @@ def launch_context(
|
|||||||
timezone: str | None = None,
|
timezone: str | None = None,
|
||||||
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
|
color_scheme: Literal["light", "dark", "no-preference"] | None = None,
|
||||||
geoip: bool = False,
|
geoip: bool = False,
|
||||||
|
backend: str | None = None,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
"""Launch stealth browser and return a BrowserContext with common options pre-set.
|
"""Launch stealth browser and return a BrowserContext with common options pre-set.
|
||||||
@@ -414,8 +426,8 @@ def launch_context(
|
|||||||
timezone: IANA timezone (e.g. 'America/New_York').
|
timezone: IANA timezone (e.g. 'America/New_York').
|
||||||
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
|
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
|
||||||
Default: None (uses Chromium default, which is 'light').
|
Default: None (uses Chromium default, which is 'light').
|
||||||
Note: 'no-preference' doesn't work in Patchright (falls back to 'light').
|
|
||||||
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
geoip: Auto-detect timezone/locale from proxy IP (default False).
|
||||||
|
backend: Playwright backend — 'playwright' (default) or 'patchright'.
|
||||||
**kwargs: Passed to browser.new_context().
|
**kwargs: Passed to browser.new_context().
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
@@ -430,7 +442,7 @@ def launch_context(
|
|||||||
# context and interferes with Playwright's timezone_id on new contexts.
|
# context and interferes with Playwright's timezone_id on new contexts.
|
||||||
# Timezone is set via browser.new_context(timezone_id=...) below instead.
|
# Timezone is set via browser.new_context(timezone_id=...) below instead.
|
||||||
browser = launch(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args,
|
browser = launch(headless=headless, proxy=proxy, args=args, stealth_args=stealth_args,
|
||||||
timezone=None, locale=locale)
|
timezone=None, locale=locale, backend=backend)
|
||||||
|
|
||||||
context_kwargs: dict[str, Any] = {}
|
context_kwargs: dict[str, Any] = {}
|
||||||
if user_agent:
|
if user_agent:
|
||||||
@@ -462,6 +474,47 @@ def launch_context(
|
|||||||
return context
|
return context
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Backend resolution
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_backend(backend: str | None) -> str:
|
||||||
|
"""Resolve backend: param > env var > default ('playwright')."""
|
||||||
|
b = backend or os.environ.get("CLOAKBROWSER_BACKEND", "playwright")
|
||||||
|
if b not in ("playwright", "patchright"):
|
||||||
|
raise ValueError(f"Unknown backend '{b}'. Use 'playwright' or 'patchright'.")
|
||||||
|
return b
|
||||||
|
|
||||||
|
|
||||||
|
def _import_sync_playwright(backend: str):
|
||||||
|
"""Import sync_playwright from the resolved backend."""
|
||||||
|
if backend == "patchright":
|
||||||
|
try:
|
||||||
|
from patchright.sync_api import sync_playwright
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
raise ModuleNotFoundError(
|
||||||
|
"patchright is not installed. Install it with: pip install cloakbrowser[patchright]"
|
||||||
|
) from None
|
||||||
|
return sync_playwright
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
return sync_playwright
|
||||||
|
|
||||||
|
|
||||||
|
def _import_async_playwright(backend: str):
|
||||||
|
"""Import async_playwright from the resolved backend."""
|
||||||
|
if backend == "patchright":
|
||||||
|
try:
|
||||||
|
from patchright.async_api import async_playwright
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
raise ModuleNotFoundError(
|
||||||
|
"patchright is not installed. Install it with: pip install cloakbrowser[patchright]"
|
||||||
|
) from None
|
||||||
|
return async_playwright
|
||||||
|
from playwright.async_api import async_playwright
|
||||||
|
return async_playwright
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Internal helpers
|
# Internal helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -496,18 +549,39 @@ def _build_args(
|
|||||||
timezone: str | None = None,
|
timezone: str | None = None,
|
||||||
locale: str | None = None,
|
locale: str | None = None,
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""Combine stealth args with user-provided args and locale flags."""
|
"""Combine stealth args with user-provided args and locale flags.
|
||||||
result = []
|
|
||||||
|
Deduplicates by flag key (everything before '=').
|
||||||
|
Priority: stealth defaults < user args < dedicated params (timezone/locale).
|
||||||
|
"""
|
||||||
|
seen: dict[str, str] = {}
|
||||||
|
|
||||||
if stealth_args:
|
if stealth_args:
|
||||||
result.extend(get_default_stealth_args())
|
for arg in get_default_stealth_args():
|
||||||
|
seen[arg.split("=", 1)[0]] = arg
|
||||||
|
|
||||||
if extra_args:
|
if extra_args:
|
||||||
result.extend(extra_args)
|
for arg in extra_args:
|
||||||
|
key = arg.split("=", 1)[0]
|
||||||
|
if key in seen:
|
||||||
|
logger.debug("Arg override: %s -> %s", seen[key], arg)
|
||||||
|
seen[key] = arg
|
||||||
|
|
||||||
# Timezone/locale flags are independent of stealth_args — always inject when set
|
# Timezone/locale flags are independent of stealth_args — always inject when set
|
||||||
if timezone:
|
if timezone:
|
||||||
result.append(f"--fingerprint-timezone={timezone}")
|
key = "--fingerprint-timezone"
|
||||||
|
flag = f"{key}={timezone}"
|
||||||
|
if key in seen:
|
||||||
|
logger.debug("Arg override: %s -> %s", seen[key], flag)
|
||||||
|
seen[key] = flag
|
||||||
if locale:
|
if locale:
|
||||||
result.append(f"--lang={locale}")
|
key = "--lang"
|
||||||
return result
|
flag = f"{key}={locale}"
|
||||||
|
if key in seen:
|
||||||
|
logger.debug("Arg override: %s -> %s", seen[key], flag)
|
||||||
|
seen[key] = flag
|
||||||
|
|
||||||
|
return list(seen.values())
|
||||||
|
|
||||||
|
|
||||||
def _parse_proxy_url(proxy: str) -> dict[str, Any]:
|
def _parse_proxy_url(proxy: str) -> dict[str, Any]:
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ 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.109.2"
|
CHROMIUM_VERSION = "145.0.7632.159.2"
|
||||||
|
|
||||||
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
|
PLATFORM_CHROMIUM_VERSIONS: dict[str, str] = {
|
||||||
"linux-x64": "145.0.7632.109.2",
|
"linux-x64": "145.0.7632.159.2",
|
||||||
"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",
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ from .config import (
|
|||||||
logger = logging.getLogger("cloakbrowser")
|
logger = logging.getLogger("cloakbrowser")
|
||||||
|
|
||||||
# Timeout for download (large binary, allow 10 min)
|
# Timeout for download (large binary, allow 10 min)
|
||||||
DOWNLOAD_TIMEOUT = 600.0
|
DOWNLOAD_TIMEOUT = httpx.Timeout(connect=10.0, read=60.0, write=10.0, pool=10.0)
|
||||||
|
|
||||||
# Auto-update check interval (1 hour)
|
# Auto-update check interval (1 hour)
|
||||||
UPDATE_CHECK_INTERVAL = 3600
|
UPDATE_CHECK_INTERVAL = 3600
|
||||||
|
|||||||
+3
-3
@@ -247,13 +247,13 @@ Other tips for maximizing reCAPTCHA scores:
|
|||||||
When auto-update downloads a newer binary, the previous version stays in `~/.cloakbrowser/`. Point `CLOAKBROWSER_BINARY_PATH` to the older cached binary:
|
When auto-update downloads a newer binary, the previous version stays in `~/.cloakbrowser/`. Point `CLOAKBROWSER_BINARY_PATH` to the older cached binary:
|
||||||
```bash
|
```bash
|
||||||
# Linux
|
# Linux
|
||||||
export CLOAKBROWSER_BINARY_PATH=~/.cloakbrowser/chromium-145.0.7632.109/chrome
|
export CLOAKBROWSER_BINARY_PATH=~/.cloakbrowser/chromium-145.0.7632.159.2/chrome
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
export CLOAKBROWSER_BINARY_PATH=~/.cloakbrowser/chromium-145.0.7632.109/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\chrome.exe
|
set CLOAKBROWSER_BINARY_PATH=%USERPROFILE%\.cloakbrowser\chromium-145.0.7632.109.2\chrome.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "cloakbrowser",
|
"name": "cloakbrowser",
|
||||||
"version": "0.3.8",
|
"version": "0.3.10",
|
||||||
"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",
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
/**
|
||||||
|
* Shared argument builder for Playwright and Puppeteer wrappers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { LaunchOptions } from "./types.js";
|
||||||
|
import { getDefaultStealthArgs } from "./config.js";
|
||||||
|
|
||||||
|
const DEBUG = /\bcloakbrowser\b/.test(process.env.DEBUG ?? "");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build deduplicated Chromium CLI args from stealth defaults + user overrides.
|
||||||
|
*
|
||||||
|
* Priority: stealth defaults < user args < dedicated params (timezone/locale).
|
||||||
|
*/
|
||||||
|
export function buildArgs(options: LaunchOptions): string[] {
|
||||||
|
const seen = new Map<string, string>();
|
||||||
|
|
||||||
|
if (options.stealthArgs !== false) {
|
||||||
|
for (const arg of getDefaultStealthArgs()) {
|
||||||
|
seen.set(arg.split("=")[0], arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.args) {
|
||||||
|
for (const arg of options.args) {
|
||||||
|
const key = arg.split("=")[0];
|
||||||
|
if (seen.has(key)) {
|
||||||
|
if (DEBUG) console.debug(`[cloakbrowser] Arg override: ${seen.get(key)} -> ${arg}`);
|
||||||
|
}
|
||||||
|
seen.set(key, arg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.timezone) {
|
||||||
|
const key = "--fingerprint-timezone";
|
||||||
|
const flag = `${key}=${options.timezone}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
if (DEBUG) console.debug(`[cloakbrowser] Arg override: ${seen.get(key)} -> ${flag}`);
|
||||||
|
}
|
||||||
|
seen.set(key, flag);
|
||||||
|
}
|
||||||
|
if (options.locale) {
|
||||||
|
const key = "--lang";
|
||||||
|
const flag = `${key}=${options.locale}`;
|
||||||
|
if (seen.has(key)) {
|
||||||
|
if (DEBUG) console.debug(`[cloakbrowser] Arg override: ${seen.get(key)} -> ${flag}`);
|
||||||
|
}
|
||||||
|
seen.set(key, flag);
|
||||||
|
}
|
||||||
|
return [...seen.values()];
|
||||||
|
}
|
||||||
+2
-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.109.2";
|
export const CHROMIUM_VERSION = "145.0.7632.159.2";
|
||||||
|
|
||||||
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
|
export const PLATFORM_CHROMIUM_VERSIONS: Record<string, string> = {
|
||||||
"linux-x64": "145.0.7632.109.2",
|
"linux-x64": "145.0.7632.159.2",
|
||||||
"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",
|
||||||
|
|||||||
+3
-22
@@ -5,7 +5,8 @@
|
|||||||
|
|
||||||
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, getDefaultStealthArgs } from "./config.js";
|
import { DEFAULT_VIEWPORT } from "./config.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";
|
||||||
|
|
||||||
@@ -176,24 +177,4 @@ async function maybeResolveGeoip(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @internal Exposed for unit tests only. */
|
/** @internal Exposed for unit tests only. */
|
||||||
export function _buildArgsForTest(options: LaunchOptions): string[] {
|
export { buildArgs as _buildArgsForTest } from "./args.js";
|
||||||
return buildArgs(options);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildArgs(options: LaunchOptions): string[] {
|
|
||||||
const args: string[] = [];
|
|
||||||
if (options.stealthArgs !== false) {
|
|
||||||
args.push(...getDefaultStealthArgs());
|
|
||||||
}
|
|
||||||
if (options.args) {
|
|
||||||
args.push(...options.args);
|
|
||||||
}
|
|
||||||
// Timezone/locale flags — always inject when set
|
|
||||||
if (options.timezone) {
|
|
||||||
args.push(`--fingerprint-timezone=${options.timezone}`);
|
|
||||||
}
|
|
||||||
if (options.locale) {
|
|
||||||
args.push(`--lang=${options.locale}`);
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|||||||
+1
-17
@@ -5,7 +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 { getDefaultStealthArgs } from "./config.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";
|
||||||
|
|
||||||
@@ -99,19 +99,3 @@ async function maybeResolveGeoip(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildArgs(options: LaunchOptions): string[] {
|
|
||||||
const args: string[] = [];
|
|
||||||
if (options.stealthArgs !== false) {
|
|
||||||
args.push(...getDefaultStealthArgs());
|
|
||||||
}
|
|
||||||
if (options.args) {
|
|
||||||
args.push(...options.args);
|
|
||||||
}
|
|
||||||
if (options.timezone) {
|
|
||||||
args.push(`--fingerprint-timezone=${options.timezone}`);
|
|
||||||
}
|
|
||||||
if (options.locale) {
|
|
||||||
args.push(`--lang=${options.locale}`);
|
|
||||||
}
|
|
||||||
return args;
|
|
||||||
}
|
|
||||||
|
|||||||
+53
-1
@@ -29,7 +29,6 @@ describe("config", () => {
|
|||||||
expect(args.some((a) => a.includes("hardware-concurrency"))).toBe(false);
|
expect(args.some((a) => a.includes("hardware-concurrency"))).toBe(false);
|
||||||
} else {
|
} else {
|
||||||
expect(args).toContain("--fingerprint-platform=windows");
|
expect(args).toContain("--fingerprint-platform=windows");
|
||||||
expect(args).toContain("--fingerprint-hardware-concurrency=8");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should have a random fingerprint seed
|
// Should have a random fingerprint seed
|
||||||
@@ -123,6 +122,59 @@ describe("buildArgs timezone/locale", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("buildArgs deduplication", () => {
|
||||||
|
it("user --fingerprint overrides default seed", () => {
|
||||||
|
const args = _buildArgsForTest({ args: ["--fingerprint=99887"] });
|
||||||
|
const fpArgs = args.filter(a => a.startsWith("--fingerprint="));
|
||||||
|
expect(fpArgs).toHaveLength(1);
|
||||||
|
expect(fpArgs[0]).toBe("--fingerprint=99887");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("user --fingerprint-platform overrides default", () => {
|
||||||
|
const args = _buildArgsForTest({ args: ["--fingerprint-platform=linux"] });
|
||||||
|
const platArgs = args.filter(a => a.startsWith("--fingerprint-platform="));
|
||||||
|
expect(platArgs).toHaveLength(1);
|
||||||
|
expect(platArgs[0]).toBe("--fingerprint-platform=linux");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("timezone param overrides user --fingerprint-timezone arg", () => {
|
||||||
|
const args = _buildArgsForTest({
|
||||||
|
args: ["--fingerprint-timezone=Europe/London"],
|
||||||
|
timezone: "America/New_York",
|
||||||
|
});
|
||||||
|
const tzArgs = args.filter(a => a.startsWith("--fingerprint-timezone="));
|
||||||
|
expect(tzArgs).toHaveLength(1);
|
||||||
|
expect(tzArgs[0]).toBe("--fingerprint-timezone=America/New_York");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("locale param overrides user --lang arg", () => {
|
||||||
|
const args = _buildArgsForTest({
|
||||||
|
args: ["--lang=de-DE"],
|
||||||
|
locale: "en-US",
|
||||||
|
});
|
||||||
|
const langArgs = args.filter(a => a.startsWith("--lang="));
|
||||||
|
expect(langArgs).toHaveLength(1);
|
||||||
|
expect(langArgs[0]).toBe("--lang=en-US");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("no duplicate flag keys in output", () => {
|
||||||
|
const args = _buildArgsForTest({
|
||||||
|
args: ["--fingerprint=99887", "--fingerprint-timezone=UTC", "--lang=fr-FR"],
|
||||||
|
timezone: "Europe/Berlin",
|
||||||
|
locale: "de-DE",
|
||||||
|
});
|
||||||
|
const keys = args.map(a => a.split("=")[0]);
|
||||||
|
expect(new Set(keys).size).toBe(keys.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("non-value flags preserved without dedup issues", () => {
|
||||||
|
const args = _buildArgsForTest({ args: ["--disable-gpu", "--no-zygote"] });
|
||||||
|
expect(args).toContain("--disable-gpu");
|
||||||
|
expect(args).toContain("--no-zygote");
|
||||||
|
expect(args).toContain("--no-sandbox");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("migrateTimezoneId deprecation", () => {
|
describe("migrateTimezoneId deprecation", () => {
|
||||||
it("migrates timezoneId to timezone", () => {
|
it("migrates timezoneId to timezone", () => {
|
||||||
const result = migrateTimezoneId({ timezoneId: "Europe/Paris" });
|
const result = migrateTimezoneId({ timezoneId: "Europe/Paris" });
|
||||||
|
|||||||
+2
-1
@@ -49,12 +49,13 @@ classifiers = [
|
|||||||
"Topic :: Software Development :: Testing",
|
"Topic :: Software Development :: Testing",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"patchright>=1.40",
|
"playwright>=1.40",
|
||||||
"httpx>=0.24",
|
"httpx>=0.24",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
geoip = ["geoip2>=4.0"]
|
geoip = ["geoip2>=4.0"]
|
||||||
|
patchright = ["patchright>=1.40"]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
Homepage = "https://github.com/CloakHQ/CloakBrowser"
|
Homepage = "https://github.com/CloakHQ/CloakBrowser"
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
"""Shared test fixtures."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _clean_backend_env(monkeypatch):
|
||||||
|
"""Ensure CLOAKBROWSER_BACKEND doesn't leak into tests from the host environment."""
|
||||||
|
monkeypatch.delenv("CLOAKBROWSER_BACKEND", raising=False)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""Unit tests for backend resolution (_resolve_backend)."""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from cloakbrowser.browser import _resolve_backend
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_backend_default():
|
||||||
|
"""No param, no env var → 'playwright'."""
|
||||||
|
with patch.dict(os.environ, {}, clear=True):
|
||||||
|
assert _resolve_backend(None) == "playwright"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_backend_explicit_playwright():
|
||||||
|
assert _resolve_backend("playwright") == "playwright"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_backend_explicit_patchright():
|
||||||
|
assert _resolve_backend("patchright") == "patchright"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_backend_env_var():
|
||||||
|
"""CLOAKBROWSER_BACKEND env var used when no param."""
|
||||||
|
with patch.dict(os.environ, {"CLOAKBROWSER_BACKEND": "patchright"}):
|
||||||
|
assert _resolve_backend(None) == "patchright"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_backend_param_beats_env():
|
||||||
|
"""Explicit param overrides env var."""
|
||||||
|
with patch.dict(os.environ, {"CLOAKBROWSER_BACKEND": "patchright"}):
|
||||||
|
assert _resolve_backend("playwright") == "playwright"
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_backend_invalid_raises():
|
||||||
|
with pytest.raises(ValueError, match="Unknown backend 'bogus'"):
|
||||||
|
_resolve_backend("bogus")
|
||||||
|
|
||||||
|
|
||||||
|
def test_resolve_backend_invalid_env_raises():
|
||||||
|
with patch.dict(os.environ, {"CLOAKBROWSER_BACKEND": "bogus"}):
|
||||||
|
with pytest.raises(ValueError, match="Unknown backend 'bogus'"):
|
||||||
|
_resolve_backend(None)
|
||||||
@@ -92,3 +92,75 @@ def test_migrate_both_none():
|
|||||||
result = _migrate_timezone_id(None, kwargs)
|
result = _migrate_timezone_id(None, kwargs)
|
||||||
assert result is None
|
assert result is None
|
||||||
assert len(w) == 0
|
assert len(w) == 0
|
||||||
|
|
||||||
|
|
||||||
|
# --- Deduplication tests ---
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_fingerprint_overrides_default():
|
||||||
|
"""User --fingerprint should override the random default seed."""
|
||||||
|
args = _build_args(stealth_args=True, extra_args=["--fingerprint=99887"])
|
||||||
|
fingerprint_args = [a for a in args if a.startswith("--fingerprint=")]
|
||||||
|
assert len(fingerprint_args) == 1
|
||||||
|
assert fingerprint_args[0] == "--fingerprint=99887"
|
||||||
|
|
||||||
|
|
||||||
|
def test_user_platform_overrides_default():
|
||||||
|
"""User --fingerprint-platform should override the default."""
|
||||||
|
args = _build_args(stealth_args=True, extra_args=["--fingerprint-platform=linux"])
|
||||||
|
platform_args = [a for a in args if a.startswith("--fingerprint-platform=")]
|
||||||
|
assert len(platform_args) == 1
|
||||||
|
assert platform_args[0] == "--fingerprint-platform=linux"
|
||||||
|
|
||||||
|
|
||||||
|
def test_timezone_param_overrides_user_arg():
|
||||||
|
"""Dedicated timezone param should override user arg."""
|
||||||
|
args = _build_args(
|
||||||
|
stealth_args=True,
|
||||||
|
extra_args=["--fingerprint-timezone=Europe/London"],
|
||||||
|
timezone="America/New_York",
|
||||||
|
)
|
||||||
|
tz_args = [a for a in args if a.startswith("--fingerprint-timezone=")]
|
||||||
|
assert len(tz_args) == 1
|
||||||
|
assert tz_args[0] == "--fingerprint-timezone=America/New_York"
|
||||||
|
|
||||||
|
|
||||||
|
def test_locale_param_overrides_user_arg():
|
||||||
|
"""Dedicated locale param should override user --lang arg."""
|
||||||
|
args = _build_args(
|
||||||
|
stealth_args=True,
|
||||||
|
extra_args=["--lang=de-DE"],
|
||||||
|
locale="en-US",
|
||||||
|
)
|
||||||
|
lang_args = [a for a in args if a.startswith("--lang=")]
|
||||||
|
assert len(lang_args) == 1
|
||||||
|
assert lang_args[0] == "--lang=en-US"
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_duplicate_flags():
|
||||||
|
"""No flag key should appear more than once in the output."""
|
||||||
|
args = _build_args(
|
||||||
|
stealth_args=True,
|
||||||
|
extra_args=["--fingerprint=99887", "--fingerprint-timezone=UTC", "--lang=fr-FR"],
|
||||||
|
timezone="Europe/Berlin",
|
||||||
|
locale="de-DE",
|
||||||
|
)
|
||||||
|
keys = [a.split("=", 1)[0] for a in args]
|
||||||
|
assert len(keys) == len(set(keys)), f"Duplicate keys found: {keys}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_non_value_flags_preserved():
|
||||||
|
"""Flags without = should be preserved without dedup issues."""
|
||||||
|
args = _build_args(stealth_args=True, extra_args=["--disable-gpu", "--no-zygote"])
|
||||||
|
assert "--disable-gpu" in args
|
||||||
|
assert "--no-zygote" in args
|
||||||
|
assert "--no-sandbox" in args
|
||||||
|
|
||||||
|
|
||||||
|
def test_override_logs_debug(caplog):
|
||||||
|
"""Should log debug message when an override happens."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
with caplog.at_level(logging.DEBUG, logger="cloakbrowser"):
|
||||||
|
_build_args(stealth_args=True, extra_args=["--fingerprint=99887"])
|
||||||
|
assert any("--fingerprint=" in r.message and "99887" in r.message for r in caplog.records)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"""Unit tests for launch_persistent_context() and launch_persistent_context_async().
|
"""Unit tests for launch_persistent_context() and launch_persistent_context_async().
|
||||||
|
|
||||||
All tests mock patchright to avoid needing a binary.
|
All tests mock playwright to avoid needing a binary.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import warnings
|
import warnings
|
||||||
@@ -32,7 +32,7 @@ def test_persistent_context_args_built(_mock_geoip, _mock_bin):
|
|||||||
"""Stealth args + extra args combined correctly."""
|
"""Stealth args + extra args combined correctly."""
|
||||||
pw_cm, pw, context = _make_mock_pw_and_context()
|
pw_cm, pw, context = _make_mock_pw_and_context()
|
||||||
|
|
||||||
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
|
with patch("playwright.sync_api.sync_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context
|
from cloakbrowser.browser import launch_persistent_context
|
||||||
launch_persistent_context("/tmp/profile", args=["--disable-gpu"])
|
launch_persistent_context("/tmp/profile", args=["--disable-gpu"])
|
||||||
|
|
||||||
@@ -48,7 +48,7 @@ def test_persistent_context_default_viewport(_mock_geoip, _mock_bin):
|
|||||||
"""DEFAULT_VIEWPORT applied when no viewport given."""
|
"""DEFAULT_VIEWPORT applied when no viewport given."""
|
||||||
pw_cm, pw, context = _make_mock_pw_and_context()
|
pw_cm, pw, context = _make_mock_pw_and_context()
|
||||||
|
|
||||||
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
|
with patch("playwright.sync_api.sync_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context
|
from cloakbrowser.browser import launch_persistent_context
|
||||||
launch_persistent_context("/tmp/profile")
|
launch_persistent_context("/tmp/profile")
|
||||||
|
|
||||||
@@ -63,7 +63,7 @@ def test_persistent_context_custom_viewport(_mock_geoip, _mock_bin):
|
|||||||
pw_cm, pw, context = _make_mock_pw_and_context()
|
pw_cm, pw, context = _make_mock_pw_and_context()
|
||||||
custom = {"width": 1280, "height": 720}
|
custom = {"width": 1280, "height": 720}
|
||||||
|
|
||||||
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
|
with patch("playwright.sync_api.sync_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context
|
from cloakbrowser.browser import launch_persistent_context
|
||||||
launch_persistent_context("/tmp/profile", viewport=custom)
|
launch_persistent_context("/tmp/profile", viewport=custom)
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ def test_persistent_context_user_agent(_mock_geoip, _mock_bin):
|
|||||||
"""user_agent forwarded to launch_persistent_context()."""
|
"""user_agent forwarded to launch_persistent_context()."""
|
||||||
pw_cm, pw, context = _make_mock_pw_and_context()
|
pw_cm, pw, context = _make_mock_pw_and_context()
|
||||||
|
|
||||||
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
|
with patch("playwright.sync_api.sync_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context
|
from cloakbrowser.browser import launch_persistent_context
|
||||||
launch_persistent_context("/tmp/profile", user_agent="Custom/1.0")
|
launch_persistent_context("/tmp/profile", user_agent="Custom/1.0")
|
||||||
|
|
||||||
@@ -90,7 +90,7 @@ def test_persistent_context_locale_and_timezone(_mock_bin):
|
|||||||
"""Both timezone and locale flow to context kwargs and binary args."""
|
"""Both timezone and locale flow to context kwargs and binary args."""
|
||||||
pw_cm, pw, context = _make_mock_pw_and_context()
|
pw_cm, pw, context = _make_mock_pw_and_context()
|
||||||
|
|
||||||
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
|
with patch("playwright.sync_api.sync_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context
|
from cloakbrowser.browser import launch_persistent_context
|
||||||
launch_persistent_context("/tmp/profile", timezone="Asia/Tokyo", locale="ja-JP")
|
launch_persistent_context("/tmp/profile", timezone="Asia/Tokyo", locale="ja-JP")
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ def test_persistent_context_color_scheme(_mock_geoip, _mock_bin):
|
|||||||
"""color_scheme forwarded correctly."""
|
"""color_scheme forwarded correctly."""
|
||||||
pw_cm, pw, context = _make_mock_pw_and_context()
|
pw_cm, pw, context = _make_mock_pw_and_context()
|
||||||
|
|
||||||
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
|
with patch("playwright.sync_api.sync_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context
|
from cloakbrowser.browser import launch_persistent_context
|
||||||
launch_persistent_context("/tmp/profile", color_scheme="dark")
|
launch_persistent_context("/tmp/profile", color_scheme="dark")
|
||||||
|
|
||||||
@@ -123,7 +123,7 @@ def test_persistent_context_geoip(_mock_bin, _mock_geoip):
|
|||||||
"""geoip fills missing tz/locale."""
|
"""geoip fills missing tz/locale."""
|
||||||
pw_cm, pw, context = _make_mock_pw_and_context()
|
pw_cm, pw, context = _make_mock_pw_and_context()
|
||||||
|
|
||||||
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
|
with patch("playwright.sync_api.sync_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context
|
from cloakbrowser.browser import launch_persistent_context
|
||||||
launch_persistent_context("/tmp/profile", proxy="http://proxy:8080", geoip=True)
|
launch_persistent_context("/tmp/profile", proxy="http://proxy:8080", geoip=True)
|
||||||
|
|
||||||
@@ -137,7 +137,7 @@ def test_persistent_context_timezone_id_deprecation(_mock_bin):
|
|||||||
"""Old timezone_id kwarg migrated with warning."""
|
"""Old timezone_id kwarg migrated with warning."""
|
||||||
pw_cm, pw, context = _make_mock_pw_and_context()
|
pw_cm, pw, context = _make_mock_pw_and_context()
|
||||||
|
|
||||||
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
|
with patch("playwright.sync_api.sync_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context
|
from cloakbrowser.browser import launch_persistent_context
|
||||||
with warnings.catch_warnings(record=True) as w:
|
with warnings.catch_warnings(record=True) as w:
|
||||||
warnings.simplefilter("always")
|
warnings.simplefilter("always")
|
||||||
@@ -156,7 +156,7 @@ def test_persistent_context_close_stops_pw(_mock_geoip, _mock_bin):
|
|||||||
pw_cm, pw, context = _make_mock_pw_and_context()
|
pw_cm, pw, context = _make_mock_pw_and_context()
|
||||||
original_close = context.close
|
original_close = context.close
|
||||||
|
|
||||||
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
|
with patch("playwright.sync_api.sync_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context
|
from cloakbrowser.browser import launch_persistent_context
|
||||||
ctx = launch_persistent_context("/tmp/profile")
|
ctx = launch_persistent_context("/tmp/profile")
|
||||||
|
|
||||||
@@ -171,7 +171,7 @@ def test_persistent_context_proxy_string(_mock_geoip, _mock_bin):
|
|||||||
"""Proxy string parsed and passed."""
|
"""Proxy string parsed and passed."""
|
||||||
pw_cm, pw, context = _make_mock_pw_and_context()
|
pw_cm, pw, context = _make_mock_pw_and_context()
|
||||||
|
|
||||||
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
|
with patch("playwright.sync_api.sync_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context
|
from cloakbrowser.browser import launch_persistent_context
|
||||||
launch_persistent_context("/tmp/profile", proxy="http://user:pass@proxy:8080")
|
launch_persistent_context("/tmp/profile", proxy="http://user:pass@proxy:8080")
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ def test_persistent_context_proxy_dict(_mock_geoip, _mock_bin):
|
|||||||
pw_cm, pw, context = _make_mock_pw_and_context()
|
pw_cm, pw, context = _make_mock_pw_and_context()
|
||||||
proxy_dict = {"server": "http://proxy:8080", "bypass": ".google.com"}
|
proxy_dict = {"server": "http://proxy:8080", "bypass": ".google.com"}
|
||||||
|
|
||||||
with patch("patchright.sync_api.sync_playwright", return_value=pw_cm):
|
with patch("playwright.sync_api.sync_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context
|
from cloakbrowser.browser import launch_persistent_context
|
||||||
launch_persistent_context("/tmp/profile", proxy=proxy_dict)
|
launch_persistent_context("/tmp/profile", proxy=proxy_dict)
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ async def test_persistent_context_async_args_built(_mock_geoip, _mock_bin):
|
|||||||
"""Async launch builds args correctly."""
|
"""Async launch builds args correctly."""
|
||||||
pw_cm, pw, context = _make_mock_async_pw_and_context()
|
pw_cm, pw, context = _make_mock_async_pw_and_context()
|
||||||
|
|
||||||
with patch("patchright.async_api.async_playwright", return_value=pw_cm):
|
with patch("playwright.async_api.async_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context_async
|
from cloakbrowser.browser import launch_persistent_context_async
|
||||||
await launch_persistent_context_async("/tmp/profile", args=["--disable-gpu"])
|
await launch_persistent_context_async("/tmp/profile", args=["--disable-gpu"])
|
||||||
|
|
||||||
@@ -235,7 +235,7 @@ async def test_persistent_context_async_close_stops_pw(_mock_geoip, _mock_bin):
|
|||||||
pw_cm, pw, context = _make_mock_async_pw_and_context()
|
pw_cm, pw, context = _make_mock_async_pw_and_context()
|
||||||
original_close = context.close
|
original_close = context.close
|
||||||
|
|
||||||
with patch("patchright.async_api.async_playwright", return_value=pw_cm):
|
with patch("playwright.async_api.async_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context_async
|
from cloakbrowser.browser import launch_persistent_context_async
|
||||||
ctx = await launch_persistent_context_async("/tmp/profile")
|
ctx = await launch_persistent_context_async("/tmp/profile")
|
||||||
|
|
||||||
@@ -250,7 +250,7 @@ async def test_persistent_context_async_timezone_id_deprecation(_mock_bin):
|
|||||||
"""Deprecated timezone_id kwarg migrated with warning in async path."""
|
"""Deprecated timezone_id kwarg migrated with warning in async path."""
|
||||||
pw_cm, pw, context = _make_mock_async_pw_and_context()
|
pw_cm, pw, context = _make_mock_async_pw_and_context()
|
||||||
|
|
||||||
with patch("patchright.async_api.async_playwright", return_value=pw_cm):
|
with patch("playwright.async_api.async_playwright", return_value=pw_cm):
|
||||||
from cloakbrowser.browser import launch_persistent_context_async
|
from cloakbrowser.browser import launch_persistent_context_async
|
||||||
with warnings.catch_warnings(record=True) as w:
|
with warnings.catch_warnings(record=True) as w:
|
||||||
warnings.simplefilter("always")
|
warnings.simplefilter("always")
|
||||||
|
|||||||
@@ -217,3 +217,70 @@ class TestBotDetectionSites:
|
|||||||
score = results["score"]
|
score = results["score"]
|
||||||
assert score is not None, "Could not extract reCAPTCHA score"
|
assert score is not None, "Could not extract reCAPTCHA score"
|
||||||
assert score >= 0.7, f"reCAPTCHA score too low: {score}"
|
assert score >= 0.7, f"reCAPTCHA score too low: {score}"
|
||||||
|
|
||||||
|
|
||||||
|
class TestIssueRegressions:
|
||||||
|
"""Regression tests for specific GitHub issues.
|
||||||
|
|
||||||
|
Uses the shared browser fixture to avoid "Sync API inside asyncio loop"
|
||||||
|
errors when pytest-asyncio is active.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
|
def test_immediate_goto_works(self, browser):
|
||||||
|
"""Issue #9: page.goto() immediately after launch must not fail.
|
||||||
|
|
||||||
|
User reported reCAPTCHA fails if goto is called too quickly after
|
||||||
|
launch. This test verifies that immediate navigation works without
|
||||||
|
needing an artificial delay.
|
||||||
|
"""
|
||||||
|
page = browser.new_page()
|
||||||
|
# No delay — goto immediately
|
||||||
|
page.goto("https://example.com", timeout=30000)
|
||||||
|
title = page.title()
|
||||||
|
page.close()
|
||||||
|
assert "Example Domain" in title, f"Immediate goto failed, title={title}"
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
|
def test_add_init_script_without_proxy(self, browser):
|
||||||
|
"""Issue #27: add_init_script must work (baseline without proxy).
|
||||||
|
|
||||||
|
The bug is proxy + add_init_script, but we first verify init_script
|
||||||
|
alone works so we have a baseline.
|
||||||
|
"""
|
||||||
|
page = browser.new_page()
|
||||||
|
page.add_init_script("window.__cloaktest = 42;")
|
||||||
|
page.goto("https://example.com", timeout=30000)
|
||||||
|
val = page.evaluate("window.__cloaktest")
|
||||||
|
page.close()
|
||||||
|
assert val == 42, f"add_init_script failed, got {val}"
|
||||||
|
|
||||||
|
@pytest.mark.slow
|
||||||
|
def test_add_init_script_with_proxy(self, browser):
|
||||||
|
"""Issue #27: add_init_script + proxy must not cause ERR_TUNNEL_CONNECTION_FAILED.
|
||||||
|
|
||||||
|
Patchright bug: add_init_script breaks proxy auth. This test guards
|
||||||
|
against regression if/when the upstream fix lands. Uses context-level
|
||||||
|
proxy to avoid launching a separate browser (event loop conflict).
|
||||||
|
"""
|
||||||
|
proxy = os.environ.get("CLOAKBROWSER_TEST_PROXY")
|
||||||
|
if not proxy:
|
||||||
|
pytest.skip("CLOAKBROWSER_TEST_PROXY not set")
|
||||||
|
|
||||||
|
ctx = browser.new_context(proxy={"server": proxy})
|
||||||
|
page = ctx.new_page()
|
||||||
|
page.add_init_script("window.__cloaktest = 99;")
|
||||||
|
try:
|
||||||
|
page.goto("https://httpbin.org/ip", timeout=30000)
|
||||||
|
body = page.evaluate("document.body.innerText")
|
||||||
|
val = page.evaluate("window.__cloaktest")
|
||||||
|
assert val == 99, f"init_script value wrong: {val}"
|
||||||
|
assert "origin" in body, f"Page didn't load through proxy: {body[:100]}"
|
||||||
|
except Exception as e:
|
||||||
|
err = str(e)
|
||||||
|
if "ERR_TUNNEL_CONNECTION_FAILED" in err:
|
||||||
|
pytest.xfail("Known patchright bug: add_init_script + proxy auth (issue #27)")
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
page.close()
|
||||||
|
ctx.close()
|
||||||
|
|||||||
Reference in New Issue
Block a user