wa-dev-tools/docs/sessions/2026-09-05-0231.md
treamz dabc26d70f Sync prod: owner-реестр результатов, общая фабрика лимитов, crash-guard ffmpeg/gs, водяной знак PDF
Незакоммиченная работа сессии 2026-09-05 (уже на проде):
- lib/owner.js — реестр владельцев файлов-результатов, owner-check во всех download-роутах (IDOR)
- lib/limits.js — createLimiter(): skip для админа, ipKeyGenerator (фикс ERR_ERL_KEY_GEN_IPV6)
- video: обработчики 'error' у ffmpeg/ffprobe (spawn ENOENT больше не роняет сервер), таймауты
- pdf: runGhostscript/gsUnavailable, водяной знак с кириллицей (fontkit + TTF)
- снятие лимитов размера/количества для админа в pdf/video
- журнал сессии docs/sessions/2026-09-05-0231.md
2026-09-22 20:37:39 +03:00

11 KiB
Raw Blame History

2026-09-05 02:31 — ffmpeg/ghostscript в образ, водяной знак PDF, снятие лимитов админа

Сделано:

  • Найдена причина «Ошибка загрузки» видео: в контейнере не было ни ffmpeg/ffprobe, ни gs (контейнер поднимался из голого node:20-slim). В getVideoDuration() не было обработчика 'error' → spawn ENOENT уходил в uncaughtException → process.exit(1), то есть загрузка видео роняла весь сервер. Лимиты размера были ни при чём.
  • Контейнер переведён на собственный образ: создан /opt/sites/wadevelop/Dockerfile (FROM node:20-slim + ffmpeg + ghostscript), в /opt/sites/docker-compose.yml сервис wadevelop переключён с image: node:20-slim на build: ./wadevelop + image: wadevelop-node:latest. Образ собран, контейнер пересоздан. Проверено на бою: gs 10.00.0, ffmpeg с libx264/libvpx/libmp3lame/aac, тестовые mp4 и gif собираются.
  • PDF: водяной знак не работал вообще — в public/pdf.html в sendRequest() отсутствовала ветка case 'watermark', запрос падал в default («Неизвестная операция: watermark»). Добавлена ветка, параметры сделаны опциональными, кириллица — через встраиваемый TTF (@pdf-lib/fontkit + Manrope), т.к. StandardFonts.Helvetica кодирует только WinAnsi.
  • Админ освобождён от всех ограничений — единая фабрика лимитов lib/limits.js (skip: isAdmin), сняты лимиты размера/количества в PDF и размера в video (включая клиентскую отсечку).
  • Закрыт IDOR на скачивании результатов во всех четырёх download-роутах через новый реестр владельцев lib/owner.js (SQLite, та же БД что и очередь) + случайные суффиксы в именах файлов.
  • Всё выкачено на прод и проверено: сайт 200, /health 200, стартовый лог чистый — спам ERR_ERL_KEY_GEN_IPV6 (4 шт. при каждом старте) исчез; реестр владельцев проверен прогоном на проде (свой true, чужой false, неизвестный false).

Изменённые / созданные файлы:

  • lib/owner.js — новый: реестр владельцев файлов-результатов (file_owners в jobs.db), claim/isOwner/displayName, авточистка старше 24 ч
  • lib/limits.js — новый: createLimiter() — общая фабрика rate-limit, skip: isAdmin, ключ через ipKeyGenerator (чинит ERR_ERL_KEY_GEN_IPV6)
  • routes/video.js — обработчики 'error' для ffmpeg/ffprobe (crash-guard), FFMPEG_MISSING → 503, таймаут VIDEO_TIMEOUT_MS (10 → 30 мин), -ss до -i и -t вместо -to, прогресс от длительности результата, валидация startTime/endTime, owner-check в /download, снят лимит размера для админа
  • routes/pdf.js — runGhostscript() + gsUnavailable() (503 вместо spawn gs ENOENT) для preview/compress/toImages, водяной знак с TTF и опциональными параметрами, owner-check в /download, crypto.randomBytes в именах, лимиты размера/количества сняты для админа
  • routes/compress.js — случайные суффиксы в именах архива и single-файла, owner.claim, owner-check в /compress/download, лимитер через общую фабрику
  • server.js — owner-check в /download/:filename
  • routes/api.js, routes/auth.js, routes/favicon.js, routes/redirects.js, routes/parser.js, routes/httpclient.js — переведены на createLimiter (в auth — skipAdmin: false)
  • public/pdf.html — ветка case 'watermark' в sendRequest(), убрана обязательность текста, метка «(необязательно)»
  • public/video.html — детект админа вынесен на загрузку страницы (раньше /auth/me спрашивался уже после проверки размера — первый большой файл всегда отклонялся), MAX_SIZE = Infinity для админа
  • package.json — добавлен @pdf-lib/fontkit
  • На VPS: /opt/sites/wadevelop/Dockerfile (новый), /opt/sites/docker-compose.yml (сервис на build)

Решения:

  • Бинарники через Dockerfile, а не apt в живом контейнере — docker exec apt-get install слетел бы при первом же пересоздании контейнера.
  • Кириллица в водяном знаке через @pdf-lib/fontkit + public/vendor/fonts/manrope-600.ttf, с откатом на Helvetica + транслитерацию, если пакет/шрифт недоступны — чтобы код работал в любой среде, а не падал 500-й.
  • Владелец файла хранится в SQLite, а не в памяти — переживает рестарт; отсутствие записи трактуется как «чужой» (записи живут 24 ч против 30 мин у самих файлов).
  • Лимит входа /auth/login для админа НЕ снят — на этапе логина сессии ещё нет, отличить админа технически не от чего, и это защита самого аккаунта от подбора.
  • Правки делались от локальной копии: перед деплоем сверены md5 всех 13 затрагиваемых файлов — VPS был идентичен git HEAD (a3fb5a2), расхождений нет.
  • Заливка — через PowerShell, а не Bash: Git Bash конвертирует POSIX-пути (/opt/... → C:/Program Files/Git/opt/...) и ломает sftp_put.py.

Открытые вопросы / следующие шаги:

  • Проверить под залогиненной сессией: водяной знак PDF (кириллица, дефолты) и конвертацию/ сжатие видео с прогрессом — у меня нет учётной записи для UI-проверки.
  • Очередь не ограничивает параллельность — queue.getPending() нигде не вызывается, каждый /video/convert спавнит ffmpeg немедленно. Пока ffmpeg отсутствовал, это не проявлялось; теперь 3–4 одновременные конвертации положат VPS.
  • /mnt/webdata/storage живёт внутри контейнера, а не в томе — пересоздание контейнера (как сегодня) обнуляет jobs.db и папки результатов. Нужен том в docker-compose.yml.
  • routes/api.js /ai/explain бьётся в 192.168.31.100:11434 (домашний PC) и 127.0.0.1:8080 — с VPS недоступно, объяснялка regex и security-аудит на проде мертвы.
  • routes/logs.js (68 строк) нигде не подключён — мёртвый файл, ссылок на /logs в HTML нет.
  • routes/api.js — req.on('close') для AbortController регистрируется после дочитывания стрима: отмена при отвале клиента не работает никогда.
  • server.js:44-47 — публичные /api/* смонтированы как app.get('/api/settings', apiRouter); путь внутри роутера не совпадает, запрос проваливается дальше и обрабатывается вторым монтированием app.use('/api', ...). Работает случайно, publicLimiter срабатывает не там.
  • Хвосты прошлого аудита: DNS-rebinding pinning (HIGH-14), обновление 36 уязвимых npm-зависимостей, сведение VPS-прод и rpi4-двойника в один git.
  • Локальные правки не закоммичены — рабочее дерево грязное (14 изменённых + 2 новых файла).

Как продолжить (для чистой сессии):

  • Контекст: прод = VPS 195.140.146.84, контейнер wadevelop, код /opt/sites/www/wadevelop.ru/, теперь собирается из /opt/sites/wadevelop/Dockerfile. rpi4 — двойник, НЕ деплоить.
  • SSH: python3 C:/Users/tream/.claude/ssh_cmd.py root@195.140.146.84 "<cmd>". Заливка: python3 C:/Users/tream/.claude/sftp_put.py root@195.140.146.84 <remote_dir> <files> — запускать через PowerShell, Bash ломает POSIX-пути.
  • Пересборка образа: cd /opt/sites && docker compose build wadevelop && docker compose up -d wadevelop. Обычный деплой без смены зависимостей: заливка файлов + docker restart wadevelop.
  • Проверка: curl -s -o /dev/null -w "%{http_code}" https://wadevelop.ru/ (200); docker logs wadevelop --tail 20; docker exec wadevelop which ffmpeg ffprobe gs.
  • Откат: бэкапы *.bak-20260905 в /opt/sites/www/wadevelop.ru/ (13 файлов) и /opt/sites/docker-compose.yml.bak-20260905.

Ссылки: