Files
robotex/tests/fingerprint_check.py
T
protokeyandClaude Opus 5.5 58100fa45d feature: UA и отпечаток задаются профилю один раз, cookies запроса заменяют профильные
- user_agent записывается в профиль при первом запросе сессии, дальше игнорируется (#87)
- переданные cookies заменяют cookies профиля (clear_cookies перед add_cookies) (#88)
- без user_agent профилю генерируется отпечаток browserforge (Linux Chrome): UA с версией
  установленного Chrome, экран и devicePixelRatio; только нативная эмуляция, без JS (#89)
- make fp-check: проверка, что отпечаток применяется и стабилен, и доля различий профилей

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:23:01 +04:00

77 lines
3.2 KiB
Python

"""Проверка отпечатков профилей: сгенерированный отпечаток применяется, стабилен внутри профиля,
и сколько профилей из выборки различаются.
Запускается внутри образа (нужны Chrome и Xvfb):
make fp-check
"""
import asyncio
import json
import sys
import tempfile
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "src"))
from api import profile # noqa: E402
from engine import AsyncStealthySession # noqa: E402
# то, что обычно входит в хеш fingerprint-библиотек и видно странице
FP_JS = """async () => {
const c = document.createElement('canvas'); c.width = 240; c.height = 60;
const x = c.getContext('2d'); x.textBaseline = 'top'; x.font = '16px Arial';
x.fillStyle = '#f60'; x.fillRect(10, 10, 100, 30); x.fillStyle = '#069'; x.fillText('Robotex fp', 4, 20);
const digest = async s => Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(s))))
.slice(0, 6).map(b => b.toString(16).padStart(2, '0')).join('');
const uad = await navigator.userAgentData.getHighEntropyValues(['fullVersionList']);
return {
ua: navigator.userAgent,
ua_ch: uad.fullVersionList.map(b => b.brand + '/' + b.version.split('.')[0]).join(','),
screen: [screen.width, screen.height, devicePixelRatio],
hwc: navigator.hardwareConcurrency,
canvas: await digest(c.toDataURL()),
};
}"""
async def measure(user_data_dir: Path) -> dict:
settings = profile.load_or_create(user_data_dir, None)
kwargs = profile.browser_kwargs(settings)
async with AsyncStealthySession(headless=False, real_chrome=True, user_data_dir=str(user_data_dir), retries=1, **kwargs) as s:
return await s.fetch("https://example.com", page_action=lambda page: page.evaluate(FP_JS))
async def main(count: int = 6) -> int:
dirs = [tempfile.TemporaryDirectory() for _ in range(count)]
try:
fps = [await measure(Path(d.name)) for d in dirs]
again = await measure(Path(dirs[0].name))
saved = [profile.load(Path(d.name)) for d in dirs]
finally:
for d in dirs:
d.cleanup()
for i, fp in enumerate(fps):
print(f"profile {i} {json.dumps(fp, ensure_ascii=False)}")
print(f"profile 0 again {json.dumps(again, ensure_ascii=False)}")
errors = []
if fps[0] != again:
errors.append("profile 0 fingerprint changed between launches")
major = profile.chrome_major()
for i, (fp, st) in enumerate(zip(fps, saved)):
want = [st["fingerprint"]["screen"]["width"], st["fingerprint"]["screen"]["height"], st["fingerprint"]["device_scale_factor"]]
if fp["screen"] != want:
errors.append(f"profile {i}: screen {fp['screen']} != saved {want}")
if f"Chrome/{major}." not in fp["ua"] or f"Google Chrome/{major}" not in fp["ua_ch"]:
errors.append(f"profile {i}: user agent does not match installed Chrome {major}")
distinct = len({json.dumps(fp, sort_keys=True) for fp in fps})
print(f"distinct fingerprints: {distinct} of {count}")
print("\n".join(errors) or "OK: generated fingerprints are applied and stable within a profile")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(asyncio.run(main()))