# tradein-browser — camoufox Playwright WS server (изолированный контейнер Firefox). # Backend подключается к этому контейнеру через playwright.firefox.connect. FROM python:3.12-slim ENV PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 # Firefox runtime libs + ca-certificates для TLS RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ --mount=type=cache,target=/var/lib/apt,sharing=locked \ apt-get update && apt-get install -y --no-install-recommends \ libgtk-3-0 \ libasound2 \ libdbus-glib-1-2 \ libx11-xcb1 \ libxtst6 \ ca-certificates \ fontconfig \ fonts-liberation \ fonts-crosextra-carlito \ fonts-crosextra-caladea \ && useradd --create-home --uid 1000 app # Замер прод-контейнера (2026-08-29): `fc-list` отдавал 8 файлов / 3 семейства # (только DejaVu Sans/Sans Mono/Serif), а камуфокс запускается с os="windows" # (см. browser/server.py, _launch_browser) — браузер, называющий себя Windows, # но не знающий Arial/Times New Roman/Calibri, легко ловится перечислением # шрифтов (стандартная анти-бот проверка). Набор шрифтов подменить нельзя, # как navigator/UA/экран/таймзону — он меряется рендерингом текста, поэтому # нужны настоящие файлы шрифтов в системе. Это устранение отдельной нестыковки # фингерпринта сама по себе, безотносительно к какому-либо конкретному замеру # отказов площадок — с текущими отказами Домклика (домен инжектируемых кук) это # не связано и их не чинит. # # fonts-liberation/fonts-crosextra-carlito/fonts-crosextra-caladea — метрически # совместимые клоны Arial/Times New Roman/Courier New (Liberation), Calibri # (Carlito) и Cambria (Caladea): те же ширины символов на кегль, просто другой # рисунок. Детект по перечню шрифтов чаще всего меряет именно ширину текста # (measureText), а не байты файла шрифта — метрический клон с алиасом # fontconfig проходит проверку «Arial есть» неотличимо от настоящего Arial. # Алиасы (Arial→Liberation Sans, Calibri→Carlito, Cambria→Caladea) кладёт сам # пакет fontconfig файлом 30-metric-aliases.conf — отдельно их не пишем. # # Настоящие шрифты Microsoft (ttf-mscorefonts-installer) сознательно НЕ ставим: # пакет качает их с SourceForge на этапе сборки и требует интерактивного # принятия EULA — хрупко и недетерминированно для CI-сборки образа. # Проверка сборки намеренно смотрит НЕ на число файлов, а на алиасы: счёт файлов # зависит от того, что притащили соседние пакеты (первая версия этой проверки # требовала >=30 файлов и покраснела на фактических 20 — порог был угадан, а не # измерен). Значение имеет ровно одно: спрашиваешь Arial — получаешь метрически # совместимый шрифт, а не «шрифта нет». Замер образа 2026-08-30: 20 файлов, # 5 семейств (Liberation Sans/Serif/Mono, Carlito, Caladea). RUN fc-cache -f \ && echo "tradein-browser: fc-list = $(fc-list | wc -l) файлов, семейств: $(fc-list : family | tr , '\n' | sort -u | wc -l)" \ && fc-match Arial | grep -qi "Liberation Sans" \ && fc-match "Times New Roman" | grep -qi "Liberation Serif" \ && fc-match "Courier New" | grep -qi "Liberation Mono" \ && fc-match Calibri | grep -qi "Carlito" \ && fc-match Cambria | grep -qi "Caladea" \ && echo "tradein-browser: метрические алиасы Arial/TNR/Courier/Calibri/Cambria на месте" WORKDIR /app # Пины, а не плавающие версии: единственный лок этого сайдкара — вот эти строки. # Без них любая пересборка образа (в том числе автоматическая, на не связанном # коммите) втягивала свежайший camoufox — а вместе с ним другой playwright, под # который написан sed-патч coreBundle.js ниже. Версии сняты с работающего # прод-контейнера tradein-browser (2026-08-29): camoufox 0.5.5, playwright # 1.60.0, aiohttp 3.14.3. playwright запинен явно, хотя и приходит транзитивно # (camoufox 0.5.5 требует playwright<1.61) — патч завязан на конкретную сборку # драйвера, диапазон тут не годится. RUN pip install --no-cache-dir \ "camoufox[geoip]==0.5.5" \ "playwright==1.60.0" \ "aiohttp==3.14.3" # ── #915/#918: null-guard + ВАЛИДНЫЕ дефолты для pageError.location (playwright 1.60.0) ─ # Firefox/juggler-бэкенд иногда отдаёт uncaught JS-ошибку страницы (реклама / # cross-origin скрипты / anti-bot challenge на avito) БЕЗ поля location. # playwright при доставке pageError-события собирает # { url: pageError.location.url, line: ...lineNumber, column: ...columnNumber } # (coreBundle.js, 2 места: BrowserContextDispatcher + trace event). # # Двухслойный баг (оба рвут protocol-соединение → "Connection closed while reading # from the driver" / TargetClosedError → server.py 500 + relaunch браузера): # 1. без guard → `pageError.location.url` бросает # `TypeError: Cannot read properties of undefined (reading 'url')`; # 2. ОДНОГО optional-chaining (`?.url`) НЕДОСТАТОЧНО (это была дыра #918): # url станет undefined, а protocol-валидатор `tString` требует string → # `ValidationError: location.url: expected string, got undefined`. # Поэтому даём валидные дефолты: url → '', line/column → 0 — проходят валидатор, # событие доставляется без краша. Профит: browser возвращает HTML challenge- # страницы вместо краша → pipeline видит firewall и штатно ротирует IP, а не # уходит в crash-storm (подтверждено прод-дебагом локального стека 2026-05-31). # # Апгрейд playwright не заблокирован, но связан: camoufox тянет playwright сам # (0.5.5 → playwright<1.61), поэтому поднимать его можно только вместе с # camoufox и только проверив, что паттерны sed ниже ещё есть в coreBundle.js. # Сборка падает (exit 1), если паттерн не найден — это и есть сигнал, что bump # сломал патч. RUN PW_BUNDLE="$(python -c 'import os,playwright;print(os.path.join(os.path.dirname(playwright.__file__),"driver","package","lib","coreBundle.js"))')" \ && sed -i -E "s/pageError\.location\.url,/pageError.location?.url ?? '',/g" "$PW_BUNDLE" \ && sed -i -E 's/pageError\.location\.lineNumber,/pageError.location?.lineNumber ?? 0,/g' "$PW_BUNDLE" \ && sed -i -E 's/pageError\.location\.columnNumber/pageError.location?.columnNumber ?? 0/g' "$PW_BUNDLE" \ && N="$(grep -cF "pageError.location?.url ?? ''" "$PW_BUNDLE")" \ && echo "tradein-browser: patched pageError.location?.url ?? '' sites: $N" \ && [ "$N" -ge 2 ] COPY server.py ./server.py # camoufox fetch качает (а) Firefox-сборку и (б) GeoLite2 mmdb (geoip=True). # Firefox идёт в $HOME/.cache → ENV HOME=/home/app кладёт его в кэш app-юзера, # куда рантайм его ищет (фикс #899). НО mmdb пишется в site-packages пакета # camoufox (root-owned после pip install) — под app это PermissionError на # GeoLite2-City.mmdb. Поэтому fetch выполняем под ROOT (пишет и mmdb, и — # благодаря ENV HOME — Firefox в /home/app/.cache), затем chown'им Firefox-кэш # на app. mmdb остаётся root-owned в site-packages (world-readable → app читает # на рантайме). Verified реальной сборкой образа 2026-05-31. ENV HOME=/home/app RUN python -m camoufox fetch && chown -R app:app /home/app/.cache USER app EXPOSE 3000 CMD ["python", "server.py"]