230 lines
8.2 KiB
Python
230 lines
8.2 KiB
Python
"""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
|
|
import time
|
|
|
|
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)
|
|
time.sleep(20) # 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...")
|
|
time.sleep(30)
|
|
|
|
# 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()
|
|
|
|
print("Launching stealth browser...", flush=True)
|
|
context = launch_context(
|
|
headless=HEADLESS,
|
|
proxy=PROXY,
|
|
args=[
|
|
"--fingerprint-screen-width=1920",
|
|
"--fingerprint-screen-height=1080",
|
|
"--fingerprint-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())
|