Compare commits

..
Author SHA1 Message Date
CloakHQ 724d49f65b test: add wrapper regression tests for issues #9, #27
docs: add Docker Compose with healthcheck, persistent profiles
via volume mount, and resource usage numbers to README
2026-03-06 07:37:19 +01:00
CloakHQ ed79560e5f feat: add cloakserve CDP server mode for Docker
Add bin/cloakserve — launches stealth Chromium with remote debugging
enabled so users can connect via connect_over_cdp() from the host.
Uses socat to forward 0.0.0.0:9222 to Chrome's localhost-only CDP port.

Usage: docker run -d -p 127.0.0.1:9222:9222 cloakhq/cloakbrowser cloakserve

Tested: all stealth checks pass via CDP, bot.sannysoft.com 54/54,
reCAPTCHA 0.9, zero detection regressions.
2026-03-06 05:20:40 +01:00
CloakHQ 829e4b881f ci: add unit test workflow for Python and JavaScript
Runs pytest and vitest on every push/PR to main.
Only unit tests — no binary required.
2026-03-06 05:13:07 +01:00
6 changed files with 267 additions and 8 deletions
+34
View File
@@ -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
+5 -2
View File
@@ -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 \
libgdk-pixbuf-2.0-0 libxss1 libxtst6 fonts-liberation \
xvfb xdotool \
curl ca-certificates \
curl ca-certificates socat \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
@@ -36,7 +36,10 @@ RUN python -c "from cloakbrowser import ensure_binary; ensure_binary()" \
# CLI shortcuts
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
COPY bin/docker-entrypoint.sh /entrypoint.sh
+92 -5
View File
@@ -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>
<br>
<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>
</p>
@@ -506,13 +506,18 @@ The wrapper auto-downloads the correct binary for your platform.
## Docker
Pre-built image on Docker Hub — no install, no setup:
Pre-built image on Docker Hub — no install, no setup.
### Quick test
```bash
# Run the stealth test suite
docker run --rm cloakhq/cloakbrowser cloaktest
```
# Run your own script
### Run a script
```bash
# Inline script
docker run --rm cloakhq/cloakbrowser python -c "
from cloakbrowser import launch
browser = launch()
@@ -522,6 +527,9 @@ print(page.title())
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
docker run --rm cloakhq/cloakbrowser python -c "
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
FROM cloakhq/cloakbrowser
Executable
+69
View File
@@ -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
View File
@@ -29,7 +29,6 @@ describe("config", () => {
expect(args.some((a) => a.includes("hardware-concurrency"))).toBe(false);
} else {
expect(args).toContain("--fingerprint-platform=windows");
expect(args).toContain("--fingerprint-hardware-concurrency=8");
}
// Should have a random fingerprint seed
+67
View File
@@ -217,3 +217,70 @@ class TestBotDetectionSites:
score = results["score"]
assert score is not None, "Could not extract reCAPTCHA 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()