- 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>
77 lines
3.2 KiB
Python
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()))
|