mirror of
https://github.com/CloakHQ/CloakBrowser.git
synced 2026-06-23 11:41:46 +02:00
feat: upgrade to Chromium v145 with Patchright CDP stealth
- Migrate from Playwright to Patchright driver for CDP leak prevention - Add screen dimension spoofing args (--fingerprint-screen-width/height, --fingerprint-taskbar-height) - Add realistic default viewport (1920x955) to avoid Playwright detection - Add color_scheme param to launch_context, add fingerprint scan test - Update READMEs for v145
This commit is contained in:
@@ -20,7 +20,8 @@
|
|||||||
|
|
||||||
Drop-in Playwright/Puppeteer replacement for Python and JavaScript. Same API, same code — just swap the import. Your browser now scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30 out of 30** stealth detection tests.
|
Drop-in Playwright/Puppeteer replacement for Python and JavaScript. Same API, same code — just swap the import. Your browser now scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30 out of 30** stealth detection tests.
|
||||||
|
|
||||||
- 🔒 **16 source-level C++ patches** — not JS injection, not config flags
|
- 🔒 **22 source-level C++ patches** — not JS injection, not config flags
|
||||||
|
- 🛡️ **CDP stealth built-in** — powered by [Patchright](https://github.com/Kaliiiiiiiiii-Vinyzu/patchright), hides Playwright's automation signals
|
||||||
- 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified
|
- 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified
|
||||||
- ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests
|
- ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests
|
||||||
- 🔄 **Drop-in replacement** — works with Playwright (Python & JS) and Puppeteer (JS)
|
- 🔄 **Drop-in replacement** — works with Playwright (Python & JS) and Puppeteer (JS)
|
||||||
@@ -79,11 +80,12 @@ On first run, the stealth Chromium binary is automatically downloaded (~200MB, c
|
|||||||
|
|
||||||
- **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, voices, media devices), while the Patchright driver eliminates CDP automation leaks (Runtime.enable, Chrome flags, console detection). Most stealth tools only do one or the other.
|
||||||
- **One line to switch** — same Playwright API, no new abstractions, no CAPTCHA-solving services.
|
- **One line to switch** — same Playwright API, no new abstractions, no CAPTCHA-solving services.
|
||||||
|
|
||||||
## Test Results
|
## Test Results
|
||||||
|
|
||||||
All tests verified against live detection services. Last tested: Feb 2026 (Chromium 142).
|
All tests verified against live detection services. Last tested: Feb 2026 (Chromium 145).
|
||||||
|
|
||||||
| Detection Service | Stock Playwright | CloakBrowser | Notes |
|
| Detection Service | Stock Playwright | CloakBrowser | Notes |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -98,7 +100,7 @@ All tests verified against live detection services. Last tested: Feb 2026 (Chrom
|
|||||||
| `navigator.webdriver` | `true` | **`false`** | Source-level patch |
|
| `navigator.webdriver` | `true` | **`false`** | Source-level patch |
|
||||||
| `navigator.plugins.length` | 0 | **5** | Real plugin list |
|
| `navigator.plugins.length` | 0 | **5** | Real plugin list |
|
||||||
| `window.chrome` | `undefined` | **`object`** | Present like real Chrome |
|
| `window.chrome` | `undefined` | **`object`** | Present like real Chrome |
|
||||||
| UA string | `HeadlessChrome` | **`Chrome/142.0.0.0`** | No headless leak |
|
| UA string | `HeadlessChrome` | **`Chrome/145.0.0.0`** | No headless leak |
|
||||||
| CDP detection | Detected | **Not detected** | `isAutomatedWithCDP: false` |
|
| CDP detection | Detected | **Not detected** | `isAutomatedWithCDP: false` |
|
||||||
| TLS fingerprint | Mismatch | **Identical to Chrome** | ja3n/ja4/akamai match |
|
| TLS fingerprint | Mismatch | **Identical to Chrome** | ja3n/ja4/akamai match |
|
||||||
|
|
||||||
@@ -140,17 +142,7 @@ CloakBrowser is a thin wrapper (Python + JavaScript) around a custom-built Chrom
|
|||||||
3. **Every launch** → Playwright or Puppeteer starts with our binary + stealth args
|
3. **Every launch** → Playwright or Puppeteer starts with our binary + stealth args
|
||||||
4. **You write code** → standard Playwright/Puppeteer API, nothing new to learn
|
4. **You write code** → standard Playwright/Puppeteer API, nothing new to learn
|
||||||
|
|
||||||
The binary includes 16 source-level patches that modify:
|
The binary includes 26 source-level patches covering canvas, WebGL, audio, fonts, GPU, screen properties, hardware reporting, and automation signal removal.
|
||||||
- Canvas fingerprint generation
|
|
||||||
- WebGL renderer output
|
|
||||||
- Audio processing fingerprint
|
|
||||||
- Font enumeration results
|
|
||||||
- Hardware concurrency reporting
|
|
||||||
- Client rect measurements
|
|
||||||
- GPU vendor/renderer strings
|
|
||||||
- WebDriver flag
|
|
||||||
- Headless detection signals
|
|
||||||
- And more...
|
|
||||||
|
|
||||||
These are compiled into the Chromium binary — not injected via JavaScript, not set via flags.
|
These are compiled into the Chromium binary — not injected via JavaScript, not set via flags.
|
||||||
|
|
||||||
@@ -462,7 +454,7 @@ page.goto("https://example.com")
|
|||||||
| Linux x64 binary | ✅ Released |
|
| Linux x64 binary | ✅ Released |
|
||||||
| macOS arm64 (Apple Silicon) | ✅ Released |
|
| macOS arm64 (Apple Silicon) | ✅ Released |
|
||||||
| macOS x64 (Intel) | ✅ Released |
|
| macOS x64 (Intel) | ✅ Released |
|
||||||
| Chromium 145 build | 🔜 In progress |
|
| Chromium 145 build | ✅ Released |
|
||||||
| JavaScript/Puppeteer + Playwright support | ✅ Released |
|
| JavaScript/Puppeteer + Playwright support | ✅ Released |
|
||||||
| Fingerprint rotation per session | ✅ Released |
|
| Fingerprint rotation per session | ✅ Released |
|
||||||
| Built-in proxy rotation | 📋 Planned |
|
| Built-in proxy rotation | 📋 Planned |
|
||||||
@@ -523,6 +515,20 @@ This runs a real headed browser rendered on a virtual display — no physical mo
|
|||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
**Reddit or similar sites show CAPTCHA / "Prove your humanity"**
|
||||||
|
|
||||||
|
Some sites (notably Reddit homepage) use HTTP/2 fingerprinting that detects Playwright's connection layer. Pass `--disable-http2` to fall back to HTTP/1.1:
|
||||||
|
|
||||||
|
```python
|
||||||
|
browser = launch(args=["--disable-http2"])
|
||||||
|
```
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const browser = await launch({ args: ['--disable-http2'] });
|
||||||
|
```
|
||||||
|
|
||||||
|
Only use this flag for sites that require it — most sites work fine with HTTP/2.
|
||||||
|
|
||||||
**Binary download fails / timeout**
|
**Binary download fails / timeout**
|
||||||
Set a custom download URL or use a local binary:
|
Set a custom download URL or use a local binary:
|
||||||
```bash
|
```bash
|
||||||
@@ -532,7 +538,7 @@ export CLOAKBROWSER_BINARY_PATH=/path/to/your/chrome
|
|||||||
**"playwright install" vs CloakBrowser binary**
|
**"playwright install" vs CloakBrowser binary**
|
||||||
You do NOT need `playwright install chromium`. CloakBrowser downloads its own binary. You only need Playwright's system deps:
|
You do NOT need `playwright install chromium`. CloakBrowser downloads its own binary. You only need Playwright's system deps:
|
||||||
```bash
|
```bash
|
||||||
playwright install-deps chromium
|
patchright install-deps chromium
|
||||||
```
|
```
|
||||||
|
|
||||||
**reCAPTCHA v3 scores are low (0.1–0.3)**
|
**reCAPTCHA v3 scores are low (0.1–0.3)**
|
||||||
|
|||||||
+10
-5
@@ -18,7 +18,7 @@ import logging
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import unquote, urlparse, urlunparse
|
from urllib.parse import unquote, urlparse, urlunparse
|
||||||
|
|
||||||
from .config import get_default_stealth_args
|
from .config import DEFAULT_VIEWPORT, get_default_stealth_args
|
||||||
from .download import ensure_binary
|
from .download import ensure_binary
|
||||||
|
|
||||||
logger = logging.getLogger("cloakbrowser")
|
logger = logging.getLogger("cloakbrowser")
|
||||||
@@ -61,7 +61,7 @@ def launch(
|
|||||||
>>> print(page.title())
|
>>> print(page.title())
|
||||||
>>> browser.close()
|
>>> browser.close()
|
||||||
"""
|
"""
|
||||||
from playwright.sync_api import sync_playwright
|
from patchright.sync_api import sync_playwright
|
||||||
|
|
||||||
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)
|
||||||
@@ -129,7 +129,7 @@ async def launch_async(
|
|||||||
>>>
|
>>>
|
||||||
>>> asyncio.run(main())
|
>>> asyncio.run(main())
|
||||||
"""
|
"""
|
||||||
from playwright.async_api import async_playwright
|
from patchright.async_api import async_playwright
|
||||||
|
|
||||||
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)
|
||||||
@@ -168,6 +168,7 @@ def launch_context(
|
|||||||
viewport: dict | None = None,
|
viewport: dict | None = None,
|
||||||
locale: str | None = None,
|
locale: str | None = None,
|
||||||
timezone_id: str | None = None,
|
timezone_id: str | None = None,
|
||||||
|
color_scheme: str | None = None,
|
||||||
geoip: bool = False,
|
geoip: bool = False,
|
||||||
**kwargs: Any,
|
**kwargs: Any,
|
||||||
) -> Any:
|
) -> Any:
|
||||||
@@ -185,6 +186,9 @@ def launch_context(
|
|||||||
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
viewport: Viewport size dict, e.g. {"width": 1920, "height": 1080}.
|
||||||
locale: Browser locale, e.g. "en-US".
|
locale: Browser locale, e.g. "en-US".
|
||||||
timezone_id: Timezone, e.g. "America/New_York".
|
timezone_id: Timezone, e.g. "America/New_York".
|
||||||
|
color_scheme: Color scheme preference — 'light', 'dark', or 'no-preference'.
|
||||||
|
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).
|
||||||
**kwargs: Passed to browser.new_context().
|
**kwargs: Passed to browser.new_context().
|
||||||
|
|
||||||
@@ -200,12 +204,13 @@ def launch_context(
|
|||||||
context_kwargs: dict[str, Any] = {}
|
context_kwargs: dict[str, Any] = {}
|
||||||
if user_agent:
|
if user_agent:
|
||||||
context_kwargs["user_agent"] = user_agent
|
context_kwargs["user_agent"] = user_agent
|
||||||
if viewport:
|
context_kwargs["viewport"] = viewport or DEFAULT_VIEWPORT
|
||||||
context_kwargs["viewport"] = viewport
|
|
||||||
if locale:
|
if locale:
|
||||||
context_kwargs["locale"] = locale
|
context_kwargs["locale"] = locale
|
||||||
if timezone_id:
|
if timezone_id:
|
||||||
context_kwargs["timezone_id"] = timezone_id
|
context_kwargs["timezone_id"] = timezone_id
|
||||||
|
if color_scheme:
|
||||||
|
context_kwargs["color_scheme"] = color_scheme
|
||||||
context_kwargs.update(kwargs)
|
context_kwargs.update(kwargs)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -45,8 +45,20 @@ def get_default_stealth_args() -> list[str]:
|
|||||||
"--fingerprint-hardware-concurrency=8",
|
"--fingerprint-hardware-concurrency=8",
|
||||||
"--fingerprint-gpu-vendor=NVIDIA Corporation",
|
"--fingerprint-gpu-vendor=NVIDIA Corporation",
|
||||||
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
|
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
|
||||||
|
"--fingerprint-taskbar-height=40",
|
||||||
|
"--fingerprint-screen-width=1920",
|
||||||
|
"--fingerprint-screen-height=1080",
|
||||||
|
"--window-size=1920,1080",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Default viewport — realistic maximized Chrome on 1080p Windows
|
||||||
|
# screen=1920x1080, availHeight=1040 (minus 40px taskbar),
|
||||||
|
# innerHeight=955 (minus ~85px Chrome UI: tabs + address bar + bookmarks)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
DEFAULT_VIEWPORT = {"width": 1920, "height": 955}
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Platform detection
|
# Platform detection
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
"""Test against fingerprint-scan.com and CreepJS.
|
||||||
|
|
||||||
|
Tests the specific headless detection signals flagged by the community:
|
||||||
|
- noTaskbar, noContentIndex, noContactsManager, noDownlinkMax
|
||||||
|
- Bot risk score (fingerprint-scan.com)
|
||||||
|
- Headless/stealth percentages (CreepJS)
|
||||||
|
- Full CreepJS signal breakdown (likeHeadless, headless, stealth)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python examples/fingerprint_scan_test.py
|
||||||
|
python examples/fingerprint_scan_test.py --proxy http://10.50.96.5:8888
|
||||||
|
python examples/fingerprint_scan_test.py --headless
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from cloakbrowser import launch_context
|
||||||
|
|
||||||
|
HEADLESS = "--headless" 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_fingerprint_scan(page):
|
||||||
|
"""fingerprint-scan.com — bot risk score + headless detection signals."""
|
||||||
|
print("=== fingerprint-scan.com ===")
|
||||||
|
page.goto("https://fingerprint-scan.com/", wait_until="domcontentloaded", timeout=30000)
|
||||||
|
page.wait_for_timeout(20000) # Castle.js needs time to compute score
|
||||||
|
|
||||||
|
# Check bot risk score
|
||||||
|
score = page.evaluate(
|
||||||
|
'document.getElementById("fingerprintScore")?.textContent || "Score not rendered"'
|
||||||
|
)
|
||||||
|
print(f"Bot Risk Score: {score}")
|
||||||
|
|
||||||
|
# Check headless detection signals
|
||||||
|
apis = page.evaluate("""() => ({
|
||||||
|
noTaskbar: screen.height === screen.availHeight,
|
||||||
|
taskbarSize: screen.height - screen.availHeight,
|
||||||
|
noContentIndex: typeof window.ContentIndex === "undefined",
|
||||||
|
noContactsManager: !("contacts" in navigator),
|
||||||
|
noDownlinkMax: !("downlinkMax" in (navigator.connection || {})),
|
||||||
|
downlinkMax: navigator.connection?.downlinkMax ?? null,
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
webdriver: navigator.webdriver,
|
||||||
|
isPlaywright: "__pwInitScripts" in window || "__playwright__binding__" in window,
|
||||||
|
webgpu: typeof navigator.gpu !== "undefined" ? "available" : "NOT_AVAILABLE",
|
||||||
|
scrollbarWidth: (() => { const d = document.createElement("div"); d.style.cssText = "overflow:scroll;width:100px;height:100px;position:absolute;top:-999px"; document.body.appendChild(d); const w = d.offsetWidth - d.clientWidth; d.remove(); return w; })()
|
||||||
|
})""")
|
||||||
|
|
||||||
|
print("\nHeadless detection signals:")
|
||||||
|
headless_fails = 0
|
||||||
|
for k, v in apis.items():
|
||||||
|
is_fail = k.startswith("no") and v is True
|
||||||
|
if is_fail:
|
||||||
|
headless_fails += 1
|
||||||
|
flag = "FAIL" if is_fail else ""
|
||||||
|
print(f" {k}: {v} {flag}")
|
||||||
|
|
||||||
|
# Extract bot test results from page
|
||||||
|
bot_tests = page.evaluate("""() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
const tests = {};
|
||||||
|
for (const key of ['WebDriver', 'Is Selenium Chrome', 'CDP Check', 'Is Playwright']) {
|
||||||
|
const match = text.match(new RegExp(key + '\\\\s+(true|false)'));
|
||||||
|
if (match) tests[key] = match[1];
|
||||||
|
}
|
||||||
|
return tests;
|
||||||
|
}""")
|
||||||
|
print("\nBot Detection Tests:")
|
||||||
|
for k, v in bot_tests.items():
|
||||||
|
status = "PASS" if v == "false" else "FAIL"
|
||||||
|
print(f" {k}: {v} [{status}]")
|
||||||
|
|
||||||
|
page.screenshot(path="/results/fingerprint-scan.png", full_page=True)
|
||||||
|
print("\nScreenshot: /results/fingerprint-scan.png")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"score": score,
|
||||||
|
"headless_fails": headless_fails,
|
||||||
|
"apis": apis,
|
||||||
|
"bot_tests": bot_tests,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_creepjs(page):
|
||||||
|
"""abrahamjuliot.github.io/creepjs — comprehensive fingerprint analysis."""
|
||||||
|
print("\n=== CreepJS ===")
|
||||||
|
page.goto(
|
||||||
|
"https://abrahamjuliot.github.io/creepjs/", wait_until="domcontentloaded", timeout=30000
|
||||||
|
)
|
||||||
|
print("Waiting 30s for CreepJS analysis...")
|
||||||
|
page.wait_for_timeout(30000)
|
||||||
|
|
||||||
|
# Extract % scores from page text (matches test-infra/matrix_tests/group3_bot_detection.py)
|
||||||
|
scores = page.evaluate("""() => {
|
||||||
|
const text = document.body.innerText;
|
||||||
|
const likeMatch = text.match(/(\\d+)%\\s*like headless/i);
|
||||||
|
const headlessMatch = text.match(/(\\d+)%\\s*headless:/i);
|
||||||
|
const stealthMatch = text.match(/(\\d+)%\\s*stealth:/i);
|
||||||
|
return {
|
||||||
|
likeHeadlessPct: likeMatch ? parseInt(likeMatch[1]) : null,
|
||||||
|
headlessPct: headlessMatch ? parseInt(headlessMatch[1]) : null,
|
||||||
|
stealthPct: stealthMatch ? parseInt(stealthMatch[1]) : null,
|
||||||
|
};
|
||||||
|
}""")
|
||||||
|
|
||||||
|
print(f"\nScores:")
|
||||||
|
print(f" like-headless: {scores['likeHeadlessPct']}% (target: <=30%)")
|
||||||
|
print(f" headless: {scores['headlessPct']}% (target: 0%)")
|
||||||
|
print(f" stealth: {scores['stealthPct']}% (target: 0%)")
|
||||||
|
|
||||||
|
# Extract full signal breakdown from window.Fingerprint.headless (CreepJS internal object)
|
||||||
|
signals = page.evaluate("""() => {
|
||||||
|
try {
|
||||||
|
const fp = window.Fingerprint;
|
||||||
|
if (!fp || !fp.headless) return null;
|
||||||
|
return {
|
||||||
|
likeHeadless: fp.headless.likeHeadless || null,
|
||||||
|
headless: fp.headless.headless || null,
|
||||||
|
stealth: fp.headless.stealth || null,
|
||||||
|
};
|
||||||
|
} catch { return null; }
|
||||||
|
}""")
|
||||||
|
|
||||||
|
if signals:
|
||||||
|
if signals.get("likeHeadless"):
|
||||||
|
print("\nlikeHeadless signals:")
|
||||||
|
fails = 0
|
||||||
|
for k, v in signals["likeHeadless"].items():
|
||||||
|
is_fail = v is True
|
||||||
|
if is_fail:
|
||||||
|
fails += 1
|
||||||
|
flag = " FAIL" if is_fail else ""
|
||||||
|
print(f" {k}: {v}{flag}")
|
||||||
|
print(f" ({fails} fails)")
|
||||||
|
|
||||||
|
if signals.get("headless"):
|
||||||
|
print("\nheadless signals:")
|
||||||
|
for k, v in signals["headless"].items():
|
||||||
|
flag = " FAIL" if v is True else ""
|
||||||
|
print(f" {k}: {v}{flag}")
|
||||||
|
|
||||||
|
if signals.get("stealth"):
|
||||||
|
print("\nstealth signals:")
|
||||||
|
for k, v in signals["stealth"].items():
|
||||||
|
flag = " FAIL" if v is True else ""
|
||||||
|
print(f" {k}: {v}{flag}")
|
||||||
|
else:
|
||||||
|
print("\n(window.Fingerprint.headless not available — signals not extracted)")
|
||||||
|
|
||||||
|
# Extract platform estimate
|
||||||
|
platform = page.evaluate("""() => {
|
||||||
|
try {
|
||||||
|
const fp = window.Fingerprint;
|
||||||
|
if (!fp || !fp.platformEstimate) return null;
|
||||||
|
return fp.platformEstimate;
|
||||||
|
} catch { return null; }
|
||||||
|
}""")
|
||||||
|
if platform:
|
||||||
|
print(f"\nPlatform estimate: {platform}")
|
||||||
|
|
||||||
|
passed = (
|
||||||
|
scores["headlessPct"] is not None
|
||||||
|
and scores["headlessPct"] <= 30
|
||||||
|
and scores["stealthPct"] is not None
|
||||||
|
and scores["stealthPct"] <= 30
|
||||||
|
)
|
||||||
|
print(f"\nVerdict: {'PASS' if passed else 'FAIL'} (<=30% headless, <=30% stealth)")
|
||||||
|
|
||||||
|
page.screenshot(path="/results/creepjs.png", full_page=True)
|
||||||
|
print("Screenshot: /results/creepjs.png")
|
||||||
|
|
||||||
|
return {**scores, "signals": signals, "platform": platform}
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("=" * 60)
|
||||||
|
print("CloakBrowser — Fingerprint & Headless Detection Tests")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"Mode: {'headless' if HEADLESS else 'headed'}")
|
||||||
|
print(f"Proxy: {PROXY or 'none'}")
|
||||||
|
print()
|
||||||
|
|
||||||
|
context = launch_context(
|
||||||
|
headless=HEADLESS,
|
||||||
|
proxy=PROXY,
|
||||||
|
args=[
|
||||||
|
"--fingerprint-screen-width=1920",
|
||||||
|
"--fingerprint-screen-height=1080",
|
||||||
|
"--timezone=Asia/Jerusalem",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
page = context.new_page()
|
||||||
|
|
||||||
|
try:
|
||||||
|
fp_result = test_fingerprint_scan(page)
|
||||||
|
creep_result = test_creepjs(page)
|
||||||
|
finally:
|
||||||
|
context.close()
|
||||||
|
|
||||||
|
# Summary
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("SUMMARY")
|
||||||
|
print("=" * 60)
|
||||||
|
print(f"fingerprint-scan.com: {fp_result['score']}")
|
||||||
|
print(f" Headless signal fails: {fp_result['headless_fails']}")
|
||||||
|
like = creep_result["likeHeadlessPct"]
|
||||||
|
headless = creep_result["headlessPct"]
|
||||||
|
stealth = creep_result["stealthPct"]
|
||||||
|
print(f"CreepJS: like-headless={like}%, headless={headless}%, stealth={stealth}%")
|
||||||
|
|
||||||
|
# Count CreepJS signal fails
|
||||||
|
sigs = creep_result.get("signals")
|
||||||
|
if sigs and sigs.get("likeHeadless"):
|
||||||
|
fail_names = [k for k, v in sigs["likeHeadless"].items() if v is True]
|
||||||
|
if fail_names:
|
||||||
|
print(f" likeHeadless fails: {', '.join(fail_names)}")
|
||||||
|
print("=" * 60)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
|
|
||||||
Drop-in Playwright/Puppeteer replacement. Same API — just swap the import. Scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30/30** stealth detection tests.
|
Drop-in Playwright/Puppeteer replacement. Same API — just swap the import. Scores **0.9 on reCAPTCHA v3**, passes **Cloudflare Turnstile**, and clears **30/30** stealth detection tests.
|
||||||
|
|
||||||
- 🔒 **16 source-level C++ patches** — not JS injection, not config flags
|
- 🔒 **22 source-level C++ patches** — not JS injection, not config flags
|
||||||
- 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified
|
- 🎯 **0.9 reCAPTCHA v3 score** — human-level, server-verified
|
||||||
- ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests
|
- ☁️ **Passes Cloudflare Turnstile**, FingerprintJS, BrowserScan — 30/30 tests
|
||||||
- 🔄 **Drop-in replacement** — works with both Playwright and Puppeteer
|
- 🔄 **Drop-in replacement** — works with both Playwright and Puppeteer
|
||||||
|
|||||||
@@ -139,6 +139,11 @@ export function getLocalBinaryOverride(): string | undefined {
|
|||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Default stealth arguments
|
// Default stealth arguments
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
// Default viewport — realistic maximized Chrome on 1080p Windows
|
||||||
|
// screen=1920x1080, availHeight=1040 (minus 40px taskbar),
|
||||||
|
// innerHeight=955 (minus ~85px Chrome UI: tabs + address bar + bookmarks)
|
||||||
|
export const DEFAULT_VIEWPORT = { width: 1920, height: 955 };
|
||||||
|
|
||||||
export function getDefaultStealthArgs(): string[] {
|
export function getDefaultStealthArgs(): string[] {
|
||||||
const seed = Math.floor(Math.random() * 90000) + 10000; // 10000-99999
|
const seed = Math.floor(Math.random() * 90000) + 10000; // 10000-99999
|
||||||
const isMac = process.platform === "darwin";
|
const isMac = process.platform === "darwin";
|
||||||
@@ -161,5 +166,9 @@ export function getDefaultStealthArgs(): string[] {
|
|||||||
"--fingerprint-hardware-concurrency=8",
|
"--fingerprint-hardware-concurrency=8",
|
||||||
"--fingerprint-gpu-vendor=NVIDIA Corporation",
|
"--fingerprint-gpu-vendor=NVIDIA Corporation",
|
||||||
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
|
"--fingerprint-gpu-renderer=NVIDIA GeForce RTX 3070",
|
||||||
|
"--fingerprint-taskbar-height=40",
|
||||||
|
"--fingerprint-screen-width=1920",
|
||||||
|
"--fingerprint-screen-height=1080",
|
||||||
|
"--window-size=1920,1080",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
import type { Browser, BrowserContext } from "playwright-core";
|
import type { Browser, BrowserContext } from "playwright-core";
|
||||||
import type { LaunchOptions, LaunchContextOptions } from "./types.js";
|
import type { LaunchOptions, LaunchContextOptions } from "./types.js";
|
||||||
import { getDefaultStealthArgs } from "./config.js";
|
import { DEFAULT_VIEWPORT, getDefaultStealthArgs } from "./config.js";
|
||||||
import { ensureBinary } from "./download.js";
|
import { ensureBinary } from "./download.js";
|
||||||
import { parseProxyUrl } from "./proxy.js";
|
import { parseProxyUrl } from "./proxy.js";
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ export async function launchContext(
|
|||||||
try {
|
try {
|
||||||
context = await browser.newContext({
|
context = await browser.newContext({
|
||||||
...(options.userAgent ? { userAgent: options.userAgent } : {}),
|
...(options.userAgent ? { userAgent: options.userAgent } : {}),
|
||||||
...(options.viewport ? { viewport: options.viewport } : {}),
|
viewport: options.viewport ?? DEFAULT_VIEWPORT,
|
||||||
...(resolved.locale ? { locale: resolved.locale } : {}),
|
...(resolved.locale ? { locale: resolved.locale } : {}),
|
||||||
...(resolved.timezone ? { timezoneId: resolved.timezone } : {}),
|
...(resolved.timezone ? { timezoneId: resolved.timezone } : {}),
|
||||||
});
|
});
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ classifiers = [
|
|||||||
"Topic :: Software Development :: Testing",
|
"Topic :: Software Development :: Testing",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"playwright>=1.40",
|
"patchright>=1.40",
|
||||||
"httpx>=0.24",
|
"httpx>=0.24",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user