Перед каждым запуском окна в Default/Preferences профиля дописываются prefs: credentials_enable_service, profile.password_manager_enabled, translate.enabled, autofill.* = false. Применяется и к новым, и к существующим профилям, остальные настройки Chrome не трогаются. Пузырь «Сохранить пароль?» и предложение перевода больше не появляются, пароли не оседают в Login Data профиля. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
120 lines
5.8 KiB
Python
120 lines
5.8 KiB
Python
"""
|
|
Отпечаток профиля сессии (рядом с профилем Chrome): экран, devicePixelRatio и seed шума canvas.
|
|
Генерируется один раз при первом запросе сессии и дальше не меняется — реальный человек не меняет
|
|
монитор между визитами.
|
|
|
|
Экран и DPR применяются нативной эмуляцией Chrome. JS — только минимальный шум canvas (js/canvas_noise.js,
|
|
#89): полная подмена через browserforge InjectFunction ломала прохождение Cloudflare Turnstile (51a801f).
|
|
Остальное (WebGL, hardwareConcurrency, шрифты) — родное, как его отдаёт Chrome через Patchright.
|
|
|
|
User-Agent не подменяется: строка UA, отличная от реального Chrome, расходится с
|
|
navigator.userAgentData и движком, и Cloudflare такой браузер не пропускает (замер в #87). UA — всегда
|
|
родной UA установленного Chrome, клиент берёт его из ответа.
|
|
"""
|
|
import json
|
|
import logging
|
|
import os
|
|
import random
|
|
from pathlib import Path
|
|
|
|
from browserforge.fingerprints import FingerprintGenerator, Screen
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
SETTINGS_FILE = "robotex_session.json"
|
|
# окно браузера под Xvfb — 1920x1080, монитор физически не может быть меньше окна
|
|
MIN_SCREEN = (1920, 1080)
|
|
CANVAS_NOISE_JS = (Path(__file__).parent / "js" / "canvas_noise.js").read_text()
|
|
# prefs Chrome, которые задаются профилю при каждом запуске (#111): без пузыря «Сохранить пароль?»,
|
|
# предложения перевода и автозаполнения — секреты клиентов не оседают в Login Data/Web Data профиля.
|
|
# --disable-translate в современных Chrome не действует, поэтому через prefs. На JS-отпечаток не влияют.
|
|
CHROME_PREFS = {
|
|
"credentials_enable_service": False,
|
|
"credentials_enable_autosignin": False,
|
|
"profile": {"password_manager_enabled": False, "password_manager_leak_detection": False},
|
|
"translate": {"enabled": False},
|
|
"autofill": {"enabled": False, "profile_enabled": False, "credit_card_enabled": False},
|
|
}
|
|
|
|
|
|
def new_canvas_seed() -> int:
|
|
return random.randint(1, 2**31 - 1)
|
|
|
|
|
|
def generate_fingerprint(attempts: int = 10) -> dict:
|
|
"""Правдоподобный отпечаток десктопного Chrome на Linux из датасета browserforge."""
|
|
screen, dpr = {"width": MIN_SCREEN[0], "height": MIN_SCREEN[1]}, 1
|
|
generator = FingerprintGenerator()
|
|
for _ in range(attempts):
|
|
# strict=True: без него browserforge при отсутствии точного совпадения тихо ослабляет
|
|
# нижнюю границу экрана и может отдать экран меньше окна браузера; цена — изредка
|
|
# ValueError, тогда просто пробуем ещё раз
|
|
try:
|
|
fp = generator.generate(
|
|
browser=["chrome"], os=["linux"], device="desktop", strict=True,
|
|
screen=Screen(min_width=MIN_SCREEN[0], min_height=MIN_SCREEN[1]),
|
|
)
|
|
except ValueError:
|
|
continue
|
|
screen, dpr = {"width": fp.screen.width, "height": fp.screen.height}, fp.screen.devicePixelRatio
|
|
break
|
|
else:
|
|
log.warning("browserforge failed %d times, using default screen", attempts)
|
|
|
|
return {"screen": screen, "device_scale_factor": dpr, "canvas_seed": new_canvas_seed()}
|
|
|
|
|
|
def load(user_data_dir: Path) -> dict:
|
|
try:
|
|
return json.loads((user_data_dir / SETTINGS_FILE).read_text())
|
|
except (OSError, json.JSONDecodeError):
|
|
return {}
|
|
|
|
|
|
def load_or_create(user_data_dir: Path) -> dict:
|
|
"""Настройки профиля: сохранённый отпечаток или новый при первом запросе сессии."""
|
|
settings = load(user_data_dir)
|
|
if "fingerprint" in settings and "canvas_seed" in settings["fingerprint"]:
|
|
return settings
|
|
if "fingerprint" in settings:
|
|
# профиль до #89: экран сохраняем, добавляем только seed шума
|
|
settings = {"fingerprint": settings["fingerprint"] | {"canvas_seed": new_canvas_seed()}}
|
|
else:
|
|
settings = {"fingerprint": generate_fingerprint()}
|
|
(user_data_dir / SETTINGS_FILE).write_text(json.dumps(settings, ensure_ascii=False))
|
|
return settings
|
|
|
|
|
|
def browser_kwargs(settings: dict) -> dict:
|
|
"""kwargs AsyncStealthySession под отпечаток профиля."""
|
|
fp = settings["fingerprint"]
|
|
return {
|
|
"additional_args": {"screen": fp["screen"], "device_scale_factor": fp["device_scale_factor"]},
|
|
"init_script_content": CANVAS_NOISE_JS.replace("__SEED__", str(int(fp["canvas_seed"]))),
|
|
}
|
|
|
|
|
|
def _merge(target: dict, source: dict) -> dict:
|
|
for key, value in source.items():
|
|
if isinstance(value, dict) and isinstance(target.get(key), dict):
|
|
_merge(target[key], value)
|
|
else:
|
|
target[key] = value
|
|
return target
|
|
|
|
|
|
def apply_chrome_prefs(user_data_dir: Path) -> None:
|
|
"""Дописать CHROME_PREFS в Default/Preferences профиля, не трогая остальное. Вызывается до запуска
|
|
Chrome: и для нового профиля, и для существующего — Chrome при выходе сохраняет эти prefs как есть."""
|
|
path = user_data_dir / "Default" / "Preferences"
|
|
try:
|
|
prefs = json.loads(path.read_text())
|
|
if not isinstance(prefs, dict):
|
|
prefs = {}
|
|
except (OSError, json.JSONDecodeError):
|
|
prefs = {}
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = path.with_name(path.name + ".robotex")
|
|
tmp.write_text(json.dumps(_merge(prefs, CHROME_PREFS), ensure_ascii=False))
|
|
os.replace(tmp, path)
|