diff --git a/src/api/fingerprint.py b/src/api/fingerprint.py deleted file mode 100644 index 30d7e1f..0000000 --- a/src/api/fingerprint.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Генерация и persist согласованного browser fingerprint на профиль (user_data_dir). - -Используем browserforge — ту же библиотеку, которую под капотом использовал Camoufox — но -поверх голого Playwright Chromium: генерируем Fingerprint (navigator/screen/WebGL/canvas/шрифты/ -headers, всё согласовано друг с другом и с выбранным UA) и инжектим его через add_init_script. - -Важно: генерируем именно под browser=['chrome'] — раз движок у нас реальный Chromium, подделывать -navigator под Firefox/Safari было бы хуже, чем не подделывать вообще (несоответствие между -заявленным UA и реальными JS-фичами движка — сильный сигнал для антибота). -""" -import json -from pathlib import Path - -from browserforge.fingerprints import Fingerprint, FingerprintGenerator, Screen -from browserforge.fingerprints.generator import NavigatorFingerprint, ScreenFingerprint, VideoCard -from browserforge.injectors.utils import InjectFunction, only_injectable_headers - -FINGERPRINT_FILE = "fingerprint.json" - - -def _fingerprint_from_dict(data: dict) -> Fingerprint: - data = dict(data) - data["screen"] = ScreenFingerprint(**data["screen"]) - data["navigator"] = NavigatorFingerprint(**data["navigator"]) - if data.get("videoCard"): - data["videoCard"] = VideoCard(**data["videoCard"]) - return Fingerprint(**data) - - -def load_or_create_fingerprint(user_data_dir: Path, **generator_kwargs) -> Fingerprint: - """Держит fingerprint стабильным для профиля: генерируется один раз при первом запросе - этой сессии (user_data_dir) и переиспользуется во всех последующих — так же, как реальный - человек не меняет видеокарту/шрифты/UA между визитами на сайт.""" - fp_file = user_data_dir / FINGERPRINT_FILE - if fp_file.exists(): - return _fingerprint_from_dict(json.loads(fp_file.read_text())) - - generator_kwargs.setdefault("browser", ["chrome"]) - # strict=True: без него browserforge при отсутствии точного совпадения в датасете - # тихо ослабляет даже нижнюю границу screen (min_width/min_height) — на практике - # так может прилететь мобильный экран под десктопный viewport (screen.availWidth - # меньше самого окна браузера — физически невозможно и легко детектится), а в - # части случаев цикл релаксации виснет на неопределённое время вместо быстрого - # ответа. Со strict=True generate() либо отдаёт честно подходящий сэмпл, либо - # сразу кидает ValueError — и то и другое быстро (~0.1с) - generator_kwargs.setdefault("strict", True) - fingerprint = FingerprintGenerator().generate(**generator_kwargs) - fp_file.write_text(fingerprint.dumps()) - return fingerprint - - -def browser_kwargs_for(fingerprint: Fingerprint, browser_name: str = "chromium") -> dict: - """kwargs для `AsyncStealthySession(**kwargs)`, полностью выставляющие браузер под fingerprint — - UA, заголовки, init-script (подделка navigator/screen/WebGL/codecs/battery) и размер окна - (viewport/screen/device_scale_factor). - - Всё уходит обычными kwargs сессии — тем же путём, что и остальная конфигурация - (proxy/cookies/locale и т.д.), как это делает сама scrapling — а не отдельным вызовом после - запуска контекста (add_init_script/set_extra_http_headers руками требуют context, который - есть только после входа в `async with AsyncStealthySession(...)`, то есть после того, как - решение о конфигурации уже "должно было" быть принято до запуска браузера). - - :param fingerprint: Сгенерированный/загруженный fingerprint (см. load_or_create_fingerprint). - :param browser_name: Имя браузера для фильтрации заголовков (only_injectable_headers) — - "chromium", раз движок у нас реальный Chromium (см. docstring модуля). - :return: dict с ключами `useragent`, `extra_headers`, `init_script_content`, `additional_args` - (viewport/screen/device_scale_factor) — распаковывается в kwargs AsyncStealthySession. - """ - screen = {"width": fingerprint.screen.width, "height": fingerprint.screen.height} - return { - "useragent": fingerprint.navigator.userAgent, - "extra_headers": only_injectable_headers(fingerprint.headers, browser_name), - "init_script_content": InjectFunction(fingerprint), - "additional_args": { - "viewport": dict(screen), - "screen": dict(screen), - "device_scale_factor": fingerprint.screen.devicePixelRatio, - }, - } diff --git a/src/main.py b/src/main.py index dce83ab..1f48c20 100644 --- a/src/main.py +++ b/src/main.py @@ -6,7 +6,6 @@ from pathlib import Path from typing import Annotated from urllib.parse import urlsplit -from browserforge.fingerprints import Screen from fastapi import Cookie from fastapi import FastAPI from fastapi import Request @@ -14,7 +13,6 @@ from fastapi import Response from patchright.async_api import Page, ProxySettings from api import actions -from api.fingerprint import browser_kwargs_for, load_or_create_fingerprint from api.geoip import resolve_geo from api.schemas import SolveRequest from engine import AsyncStealthySession @@ -115,27 +113,6 @@ async def solve( if locale: fp_kwargs["locale"] = locale - scr_size = (1920, 1080) - fingerprint = load_or_create_fingerprint( - user_data_dir, - # только нижняя граница: реальный монитор физически не может быть - # меньше открытого на нём окна браузера (viewport ниже берём из - # fingerprint'а, не отсюда) - screen=Screen(min_width=scr_size[0], min_height=scr_size[1]), - **fp_kwargs, - ) - - # UA/заголовки/init-script (подделка navigator/screen/WebGL/...) /viewport/screen/ - # device_scale_factor под fingerprint — единым куском kwargs (см. - # api/fingerprint.py:browser_kwargs_for), тем же путём, что и остальная конфигурация - # сессии (proxy/cookies/locale), а не отдельным вызовом после запуска контекста. - fp_browser_kwargs = browser_kwargs_for(fingerprint) - permissions = ["notifications"] - if geo and geo.get("geolocation"): - fp_browser_kwargs["additional_args"]["geolocation"] = geo["geolocation"] - permissions.append("geolocation") - fp_browser_kwargs["additional_args"]["permissions"] = permissions - nav_timeout_ms = payload.timeout * 1000 if payload.timeout else 60_000 blocked_domains = set(payload.blocked_domains) if payload.blocked_domains else None