Compare commits

...
3 Commits
Author SHA1 Message Date
CloakHQ 23ae521832 docs: add headed mode guide, JS roadmap, bump to 0.1.5
- Add Headed Mode section for DataDome/Cloudflare Turnstile sites
- Add Xvfb setup instructions + residential proxy tip
- Add JavaScript/Puppeteer support to roadmap
2026-02-23 18:13:07 +01:00
CloakHQ c2eb0ef21a feat: stealth hardening + test suite rewrite, bump to 0.1.4
- Remove --enable-automation via ignore_default_args (matches agent-browser
  Playwright patch — fixes connectionRTT, enables --fingerprint-* flags)
- Randomize fingerprint seed per launch (unique canvas/WebGL/audio per session)
- Change default GPU to RTX 3070 (higher market share = better blending)
- Rewrite stealth_test.py with JS evaluation for 6 detection sites
- Add --proxy flag to test script for Helper VPS routing
- All 6/6 tests passing (verified on VPS with v142 binary)
2026-02-23 07:58:50 +01:00
CloakHQ 2d7894ad68 docs: add Docker section to README, expand Dockerfile deps, bump to 0.1.3 2026-02-23 07:16:02 +01:00
8 changed files with 323 additions and 59 deletions
+4
View File
@@ -45,3 +45,7 @@ AGENTS.md
# Private docs (launch posts, strategy) # Private docs (launch posts, strategy)
docs/ docs/
# Release scripts
publish.sh
.env
+5 -2
View File
@@ -1,11 +1,14 @@
FROM python:3.12-slim FROM python:3.12-slim
# Playwright system deps # Chromium system deps (matches fingerprint-chromium 142+ requirements)
RUN apt-get update && apt-get install -y --no-install-recommends \ RUN apt-get update && apt-get install -y --no-install-recommends \
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 \ libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 \
libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 \ libdbus-1-3 libdrm2 libxkbcommon0 libatspi2.0-0 libxcomposite1 \
libxdamage1 libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \ libxdamage1 libxfixes3 libxrandr2 libgbm1 libpango-1.0-0 \
libcairo2 libasound2 libx11-xcb1 \ libcairo2 libasound2 libx11-xcb1 libfontconfig1 libx11-6 \
libxcb1 libxext6 libxshmfence1 \
libglib2.0-0 libgtk-3-0 libpangocairo-1.0-0 libcairo-gobject2 \
libgdk-pixbuf-2.0-0 libxss1 libxtst6 fonts-liberation \
&& rm -rf /var/lib/apt/lists/* && rm -rf /var/lib/apt/lists/*
WORKDIR /app WORKDIR /app
+53 -15
View File
@@ -244,12 +244,64 @@ See the [`examples/`](examples/) directory:
| Linux x64 binary | ✅ Released | | Linux x64 binary | ✅ Released |
| macOS arm64 (Apple Silicon) | 🔜 In progress | | macOS arm64 (Apple Silicon) | 🔜 In progress |
| Chromium 145 build | 🔜 In progress | | Chromium 145 build | 🔜 In progress |
| JavaScript/Puppeteer support (`cloakbrowser-js`) | 📋 Planned |
| Fingerprint rotation per session | 📋 Planned | | Fingerprint rotation per session | 📋 Planned |
| Built-in proxy rotation | 📋 Planned | | Built-in proxy rotation | 📋 Planned |
| Windows support | 📋 Planned | | Windows support | 📋 Planned |
> ⭐ **Star this repo** to get notified when Chromium 145 and macOS builds drop. > ⭐ **Star this repo** to get notified when Chromium 145 and macOS builds drop.
## Docker
A ready-to-use [`Dockerfile`](Dockerfile) is included. It installs system deps, the package, and pre-downloads the stealth binary during build:
```bash
docker build -t cloakbrowser .
docker run --rm cloakbrowser python examples/basic.py
```
The key steps in the Dockerfile:
1. **System deps** — Chromium requires ~15 shared libraries (`libnss3`, `libgbm1`, etc.)
2. **`pip install .`** — installs CloakBrowser + Playwright
3. **`ensure_binary()`** — downloads the stealth Chromium binary at build time (~200MB), so containers start instantly
To extend with your own script, just add a `COPY` + `CMD`:
```dockerfile
FROM cloakbrowser
COPY your_script.py /app/
CMD ["python", "your_script.py"]
```
**Note:** If you run CloakBrowser inside a web server with uvloop (e.g., `uvicorn[standard]`), use `--loop asyncio` to avoid subprocess pipe hangs.
## Headed Mode (for aggressive bot detection)
Some sites using advanced bot detection (e.g., DataDome, Cloudflare Turnstile) can detect headless mode even with our C++ patches. For these sites, run in **headed mode** with a virtual display:
```bash
# Install Xvfb (virtual framebuffer)
sudo apt install xvfb
# Start virtual display
Xvfb :99 -screen 0 1920x1080x24 &
export DISPLAY=:99
```
```python
from cloakbrowser import launch
# Headed mode + residential proxy for maximum stealth
browser = launch(headless=False, proxy="http://your-residential-proxy:port")
page = browser.new_page()
page.goto("https://heavily-protected-site.com") # passes DataDome, etc.
browser.close()
```
This runs a real headed browser rendered on a virtual display — no physical monitor needed. Combined with a residential proxy, this passes even the most aggressive detection services.
> **Tip:** Datacenter IPs are often flagged by IP reputation databases regardless of browser fingerprint. For sites with strict bot detection, a residential proxy makes the difference.
## Troubleshooting ## Troubleshooting
**Binary download fails / timeout** **Binary download fails / timeout**
@@ -264,20 +316,6 @@ You do NOT need `playwright install chromium`. CloakBrowser downloads its own bi
playwright install-deps chromium playwright install-deps chromium
``` ```
**Missing system libraries on Linux (Docker)**
If you see errors about `libgbm`, `libnss3`, etc.:
```bash
apt-get install -y libgbm1 libnss3 libatk-bridge2.0-0 libxkbcommon0 libgtk-3-0
```
Or use `playwright install-deps chromium` which handles this automatically.
**Pre-download binary in Docker**
```python
# In your Dockerfile or entrypoint:
from cloakbrowser import ensure_binary
ensure_binary()
```
## FAQ ## FAQ
**Q: Is this legal?** **Q: Is this legal?**
@@ -293,7 +331,7 @@ A: Possibly. Bot detection is an arms race. Source-level patches are harder to d
A: Yes. Pass `proxy="http://user:pass@host:port"` to `launch()`. A: Yes. Pass `proxy="http://user:pass@host:port"` to `launch()`.
**Q: Can I use this with Docker?** **Q: Can I use this with Docker?**
A: Yes. Use `ensure_binary()` in your Dockerfile to pre-download the binary during image build. A: Yes. A ready-to-use Dockerfile is included — see the [Docker](#docker) section above.
## License ## License
+2 -2
View File
@@ -12,7 +12,7 @@ Usage:
""" """
from .browser import launch, launch_async, launch_context from .browser import launch, launch_async, launch_context
from .config import CHROMIUM_VERSION, DEFAULT_STEALTH_ARGS from .config import CHROMIUM_VERSION, get_default_stealth_args
from .download import binary_info, clear_cache, ensure_binary from .download import binary_info, clear_cache, ensure_binary
from ._version import __version__ from ._version import __version__
@@ -24,6 +24,6 @@ __all__ = [
"clear_cache", "clear_cache",
"binary_info", "binary_info",
"CHROMIUM_VERSION", "CHROMIUM_VERSION",
"DEFAULT_STEALTH_ARGS", "get_default_stealth_args",
"__version__", "__version__",
] ]
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.1.2" __version__ = "0.1.5"
+4 -2
View File
@@ -17,7 +17,7 @@ from __future__ import annotations
import logging import logging
from typing import Any from typing import Any
from .config import DEFAULT_STEALTH_ARGS from .config import get_default_stealth_args
from .download import ensure_binary from .download import ensure_binary
logger = logging.getLogger("cloakbrowser") logger = logging.getLogger("cloakbrowser")
@@ -63,6 +63,7 @@ def launch(
executable_path=binary_path, executable_path=binary_path,
headless=headless, headless=headless,
args=chrome_args, args=chrome_args,
ignore_default_args=["--enable-automation"],
**_build_proxy_kwargs(proxy), **_build_proxy_kwargs(proxy),
**kwargs, **kwargs,
) )
@@ -123,6 +124,7 @@ async def launch_async(
executable_path=binary_path, executable_path=binary_path,
headless=headless, headless=headless,
args=chrome_args, args=chrome_args,
ignore_default_args=["--enable-automation"],
**_build_proxy_kwargs(proxy), **_build_proxy_kwargs(proxy),
**kwargs, **kwargs,
) )
@@ -209,7 +211,7 @@ def _build_args(stealth_args: bool, extra_args: list[str] | None) -> list[str]:
"""Combine stealth args with user-provided args.""" """Combine stealth args with user-provided args."""
result = [] result = []
if stealth_args: if stealth_args:
result.extend(DEFAULT_STEALTH_ARGS) result.extend(get_default_stealth_args())
if extra_args: if extra_args:
result.extend(extra_args) result.extend(extra_args)
return result return result
+13 -10
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import os import os
import platform import platform
import random
from pathlib import Path from pathlib import Path
from ._version import __version__ from ._version import __version__
@@ -17,16 +18,18 @@ CHROMIUM_VERSION = "142.0.7444.175"
# Default stealth arguments passed to the patched Chromium binary. # Default stealth arguments passed to the patched Chromium binary.
# These activate source-level fingerprint patches compiled into the binary. # These activate source-level fingerprint patches compiled into the binary.
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
DEFAULT_STEALTH_ARGS: list[str] = [ def get_default_stealth_args() -> list[str]:
"--no-sandbox", """Build stealth args with a random fingerprint seed per launch."""
"--disable-blink-features=AutomationControlled", seed = random.randint(10000, 99999)
# Fingerprint overrides (activate compiled C++ patches) return [
"--fingerprint=98765", "--no-sandbox",
"--fingerprint-platform=windows", "--disable-blink-features=AutomationControlled",
"--fingerprint-hardware-concurrency=8", f"--fingerprint={seed}",
"--fingerprint-gpu-vendor=NVIDIA Corporation", "--fingerprint-platform=windows",
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 4070", "--fingerprint-hardware-concurrency=8",
] "--fingerprint-gpu-vendor=NVIDIA Corporation",
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
]
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Platform detection # Platform detection
+241 -27
View File
@@ -1,57 +1,271 @@
"""Run stealth tests against major bot detection services. """Run stealth tests against major bot detection services.
Tests cloakbrowser against multiple detection sites and reports results. Tests cloakbrowser against multiple detection sites, extracts pass/fail
verdicts via JS evaluation, and reports results with screenshots.
Usage:
python examples/stealth_test.py
python examples/stealth_test.py --headed # watch in real-time
python examples/stealth_test.py --no-screenshots
python examples/stealth_test.py --proxy http://10.50.96.5:8888
""" """
import json
import sys
import time
from cloakbrowser import launch from cloakbrowser import launch
HEADED = "--headed" in sys.argv
SCREENSHOTS = "--no-screenshots" not in sys.argv
PROXY = None
for i, arg in enumerate(sys.argv):
if arg == "--proxy" and i + 1 < len(sys.argv):
PROXY = sys.argv[i + 1]
def test_bot_sannysoft(page):
"""bot.sannysoft.com — classic bot detection checks."""
page.goto("https://bot.sannysoft.com", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(3000)
results = page.evaluate("""() => {
const rows = document.querySelectorAll('table tr');
const data = {};
rows.forEach(r => {
const cells = r.querySelectorAll('td');
if (cells.length >= 2) {
const key = cells[0].innerText.trim();
const val = cells[1].innerText.trim();
const cls = cells[1].className || '';
data[key] = {value: val, passed: !cls.includes('failed')};
}
});
return data;
}""")
failed = [k for k, v in results.items() if not v["passed"]]
total = len(results)
passed = total - len(failed)
return {"passed": passed, "total": total, "failed": failed}
def test_bot_incolumitas(page):
"""bot.incolumitas.com — comprehensive 30+ check bot detection."""
page.goto("https://bot.incolumitas.com", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(12000) # needs time to run all detection tests
# Site outputs JSON blocks in page text, not HTML tables
results = page.evaluate("""() => {
const text = document.body.innerText;
const okMatches = text.match(/"\\w+":\\s*"OK"/g) || [];
const failMatches = text.match(/"\\w+":\\s*"FAIL"/g) || [];
const failedTests = failMatches.map(m => m.match(/"(\\w+)"/)[1]);
return {
passed: okMatches.length,
failed: failMatches.length,
failedTests,
total: okMatches.length + failMatches.length
};
}""")
return results
def test_browserscan(page):
"""browserscan.net/bot-detection — WebDriver, UA, CDP, Navigator checks."""
page.goto("https://www.browserscan.net/bot-detection", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(5000)
results = page.evaluate("""() => {
const items = document.querySelectorAll('[class*="result"], [class*="item"], [class*="check"]');
let normal = 0, abnormal = 0;
const text = document.body.innerText;
// Count "Normal" vs "Abnormal" verdicts
const normalMatches = text.match(/Normal/g);
const abnormalMatches = text.match(/Abnormal/g);
return {
normal: normalMatches ? normalMatches.length : 0,
abnormal: abnormalMatches ? abnormalMatches.length : 0,
pageText: text.substring(0, 500)
};
}""")
return results
def test_deviceandbrowserinfo(page):
"""deviceandbrowserinfo.com/are_you_a_bot — fingerprint + behavioral detection."""
page.goto("https://deviceandbrowserinfo.com/are_you_a_bot", wait_until="domcontentloaded", timeout=30000)
page.wait_for_timeout(8000)
results = page.evaluate("""() => {
const text = document.body.innerText;
// Site outputs JSON with "isBot": false and detail checks
const botMatch = text.match(/"isBot":\\s*(true|false)/);
const isBot = botMatch ? botMatch[1] === 'true' : null;
const checks = {};
const patterns = [
'isBot', 'hasBotUserAgent', 'hasWebdriverTrue',
'isHeadlessChrome', 'isAutomatedWithCDP', 'hasSuspiciousWeakSignals',
'isPlaywright', 'hasInconsistentChromeObject'
];
patterns.forEach(p => {
const match = text.match(new RegExp('"' + p + '":\\s*(true|false)'));
if (match) checks[p] = match[1] === 'true';
});
return {isBot, checks};
}""")
return results
def test_fingerprintjs(page):
"""demo.fingerprint.com/web-scraping — industry-standard bot detection."""
page.goto("https://demo.fingerprint.com/web-scraping", wait_until="networkidle", timeout=30000)
page.wait_for_timeout(5000)
# Click search to trigger bot detection — bots get blocked, humans see flights
try:
page.click("button:has-text('Search')", timeout=5000)
page.wait_for_timeout(5000)
except Exception:
pass
results = page.evaluate("""() => {
const text = document.body.innerText;
// Bots see error messages; humans see flight prices
const hasFlights = text.includes('Price per adult') || text.includes('$');
const isBlocked = text.includes('request was blocked') || text.includes('bot visit detected');
return {passed: hasFlights && !isBlocked, isBlocked, hasFlights};
}""")
return results
def test_recaptcha(page):
"""recaptcha-demo.appspot.com — Google's official reCAPTCHA v3 score."""
page.goto(
"https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php",
wait_until="networkidle",
timeout=30000,
)
# Page auto-submits via grecaptcha.execute() — wait for backend response
page.wait_for_timeout(8000)
results = page.evaluate("""() => {
const text = document.body.innerText;
// Score appears in JSON response block: "score": 0.9
const scoreMatch = text.match(/"score":\\s*(\\d+\\.\\d+)/);
return {
score: scoreMatch ? parseFloat(scoreMatch[1]) : null,
pageText: text.substring(0, 500)
};
}""")
return results
TESTS = [ TESTS = [
{
"name": "bot.sannysoft.com",
"url": "https://bot.sannysoft.com",
"runner": test_bot_sannysoft,
"verdict": lambda r: f"{r['passed']}/{r['total']} passed"
+ (f" (FAILED: {', '.join(r['failed'])})" if r["failed"] else " — ALL GREEN"),
"pass": lambda r: len(r["failed"]) == 0,
},
{ {
"name": "bot.incolumitas.com", "name": "bot.incolumitas.com",
"url": "https://bot.incolumitas.com", "url": "https://bot.incolumitas.com",
"check": "Bot detection analysis", "runner": test_bot_incolumitas,
"verdict": lambda r: f"{r['passed']}/{r['total']} passed"
+ (f" (FAILED: {', '.join(r.get('failedTests', []))})" if r.get("failed", 0) > 0 else " — ALL GREEN"),
"pass": lambda r: r.get("failed", 0) <= 1, # fpscanner.WEBDRIVER false positive expected (all builds)
}, },
{ {
"name": "BrowserScan", "name": "BrowserScan",
"url": "https://www.browserscan.net/bot-detection", "url": "https://www.browserscan.net/bot-detection",
"check": "Bot detection status", "runner": test_browserscan,
"verdict": lambda r: f"Normal: {r['normal']}, Abnormal: {r['abnormal']}",
"pass": lambda r: r.get("abnormal", 1) == 0,
}, },
{ {
"name": "deviceandbrowserinfo.com", "name": "deviceandbrowserinfo.com",
"url": "https://deviceandbrowserinfo.com/are_you_a_bot", "url": "https://deviceandbrowserinfo.com/are_you_a_bot",
"check": "isBot flag", "runner": test_deviceandbrowserinfo,
"verdict": lambda r: f"isBot: {r.get('isBot', 'unknown')}"
+ (f" checks: {json.dumps(r.get('checks', {}))}" if r.get("checks") else ""),
"pass": lambda r: not r.get("isBot", True),
}, },
{ {
"name": "FingerprintJS", "name": "FingerprintJS",
"url": "https://demo.fingerprint.com/web-scraping", "url": "https://demo.fingerprint.com/web-scraping",
"check": "Bot detection result", "runner": test_fingerprintjs,
"verdict": lambda r: "PASSED (flights shown)" if r.get("passed") else "BLOCKED" if r.get("isBlocked") else "NO FLIGHTS",
"pass": lambda r: r.get("passed", False),
},
{
"name": "reCAPTCHA v3 (Google)",
"url": "https://recaptcha-demo.appspot.com/recaptcha-v3-request-scores.php",
"runner": test_recaptcha,
"verdict": lambda r: f"Score: {r.get('score', 'N/A')}",
"pass": lambda r: (r.get("score") or 0) >= 0.7,
}, },
] ]
browser = launch(headless=True)
page = browser.new_page()
print("=" * 60) def main():
print("CloakBrowser Stealth Test Suite") print("=" * 60)
print("=" * 60) print("CloakBrowser Stealth Test Suite")
print("=" * 60)
print(f"Mode: {'headed' if HEADED else 'headless'}")
print(f"Screenshots: {'on' if SCREENSHOTS else 'off'}")
print(f"Proxy: {PROXY or 'none'}")
print()
for test in TESTS: browser = launch(headless=not HEADED, proxy=PROXY)
print(f"\n--- {test['name']} ---") page = browser.new_page()
print(f"URL: {test['url']}")
try:
page.goto(test["url"], wait_until="networkidle", timeout=30000)
page.wait_for_timeout(3000)
# Screenshot each test results_summary = []
filename = f"stealth_test_{test['name'].replace('.', '_').replace(' ', '_')}.png"
page.screenshot(path=filename)
print(f"Screenshot: {filename}")
print(f"Title: {page.title()}")
except Exception as e:
print(f"Error: {e}")
browser.close() for test in TESTS:
name = test["name"]
print(f"--- {name} ---")
print(f"URL: {test['url']}")
print("\n" + "=" * 60) try:
print("Tests complete. Check screenshots for results.") result = test["runner"](page)
print("=" * 60) passed = test["pass"](result)
verdict = test["verdict"](result)
status = "PASS" if passed else "FAIL"
results_summary.append((name, status, verdict))
print(f"Result: [{status}] {verdict}")
if SCREENSHOTS:
filename = f"stealth_test_{name.replace('.', '_').replace(' ', '_').replace('/', '_')}.png"
page.screenshot(path=filename)
print(f"Screenshot: {filename}")
except Exception as e:
results_summary.append((name, "ERROR", str(e)))
print(f"Error: {e}")
print()
browser.close()
# Summary table
print("=" * 60)
print("RESULTS SUMMARY")
print("=" * 60)
for name, status, verdict in results_summary:
icon = {"PASS": "+", "FAIL": "!", "ERROR": "x"}[status]
print(f" [{icon}] {name}: {verdict}")
passed_count = sum(1 for _, s, _ in results_summary if s == "PASS")
total = len(results_summary)
print(f"\n {passed_count}/{total} tests passed")
print("=" * 60)
return 0 if passed_count == total else 1
if __name__ == "__main__":
sys.exit(main())