feat: Docker Hub image, cloaktest CLI, and example UX improvements

Add cloakhq/cloakbrowser Docker Hub image with Node.js, JS wrapper,
Xvfb headed mode, and cloaktest shortcut. Add launch feedback and IP
display to all examples. Update README Docker section for Docker Hub.
This commit is contained in:
Cloak-HQ
2026-03-05 05:09:29 +01:00
parent 976f5ae534
commit f76dbdb044
10 changed files with 159 additions and 64 deletions
+80 -32
View File
@@ -53,21 +53,27 @@ def test_bot_sannysoft(page):
def test_bot_incolumitas(page):
"""bot.incolumitas.com — comprehensive 30+ check bot detection."""
page.goto("https://bot.incolumitas.com", wait_until="networkidle", timeout=30000)
time.sleep(12) # 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
};
}""")
# Poll until test count stabilizes (site runs tests progressively)
last_total = 0
for _ in range(15):
time.sleep(2)
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
};
}""")
if results["total"] >= 30 and results["total"] == last_total:
break
last_total = results["total"]
return results
@@ -146,23 +152,18 @@ def test_recaptcha(page):
wait_until="domcontentloaded",
timeout=30000,
)
# Wait for backend response (step3 element appears when score arrives)
try:
page.wait_for_selector("li.step3", timeout=20000)
time.sleep(1)
except Exception:
time.sleep(10) # fallback
# Wait for score to appear (polls up to 30s)
for _ in range(15):
time.sleep(2)
score = page.evaluate("""() => {
const text = document.body.innerText;
const match = text.match(/"score":\\s*(\\d+\\.\\d+)/);
return match ? parseFloat(match[1]) : null;
}""")
if score is not None:
break
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
return {"score": score}
TESTS = [
@@ -179,8 +180,11 @@ TESTS = [
"url": "https://bot.incolumitas.com",
"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)
+ (" — ALL GREEN" if r.get("failed", 0) == 0
else f" (FAILED: {', '.join(r.get('failedTests', []))} — known false positives)"
if set(r.get("failedTests", [])) <= {"WEBDRIVER", "connectionRTT"}
else f" (FAILED: {', '.join(r.get('failedTests', []))})"),
"pass": lambda r: set(r.get("failedTests", [])) <= {"WEBDRIVER", "connectionRTT"}, # known false positives
},
{
"name": "BrowserScan",
@@ -222,10 +226,54 @@ def main():
print(f"Screenshots: {'on' if SCREENSHOTS else 'off'}")
print(f"Proxy: {PROXY or 'none'}")
print()
print("Launching stealth browser...", flush=True)
browser = launch(headless=not HEADED, proxy=PROXY)
page = browser.new_page()
# Show browser fingerprint details
try:
import re
info = page.evaluate("""async () => {
const ua = navigator.userAgent;
let fullVersion = null;
try {
const data = await navigator.userAgentData.getHighEntropyValues(['fullVersionList', 'platform', 'platformVersion']);
const chrome = data.fullVersionList.find(b => b.brand === 'Chromium' || b.brand === 'Google Chrome');
fullVersion = chrome ? chrome.version : null;
} catch {}
const gl = document.createElement('canvas').getContext('webgl');
const dbg = gl ? gl.getExtension('WEBGL_debug_renderer_info') : null;
return {
ua,
fullVersion,
platform: navigator.platform,
cores: navigator.hardwareConcurrency,
gpu: dbg ? gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL) : 'N/A',
gpuVendor: dbg ? gl.getParameter(dbg.UNMASKED_VENDOR_WEBGL) : 'N/A',
screen: screen.width + 'x' + screen.height,
languages: navigator.languages.join(', '),
};
}""")
# Condensed UA
ua_short = re.sub(r'^Mozilla/5\.0 \(', '', info["ua"])
ua_short = re.sub(r'\) AppleWebKit/[\d.]+ \(KHTML, like Gecko\) ', ' | ', ua_short)
print(f"UA: {ua_short}", flush=True)
print(f"Platform: {info['platform']} | Cores: {info['cores']} | Screen: {info['screen']}", flush=True)
print(f"GPU: {info['gpuVendor']}{info['gpu']}", flush=True)
except Exception:
print("Chrome: could not detect", flush=True)
# Show IP address
try:
page.goto("https://httpbin.org/ip", timeout=10000)
ip = page.evaluate("JSON.parse(document.body.innerText).origin")
print(f"IP: {ip}", flush=True)
except Exception:
print("IP: could not detect", flush=True)
print(f"Running {len(TESTS)} tests (this takes ~2 minutes)...\n", flush=True)
results_summary = []
for test in TESTS: