Незакоммиченная работа сессии 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
104 lines
11 KiB
Markdown
104 lines
11 KiB
Markdown
# 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](../../lib/owner.js) — **новый**: реестр владельцев файлов-результатов
|
||
(`file_owners` в `jobs.db`), `claim/isOwner/displayName`, авточистка старше 24 ч
|
||
- [lib/limits.js](../../lib/limits.js) — **новый**: `createLimiter()` — общая фабрика rate-limit,
|
||
`skip: isAdmin`, ключ через `ipKeyGenerator` (чинит `ERR_ERL_KEY_GEN_IPV6`)
|
||
- [routes/video.js](../../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](../../routes/pdf.js) — `runGhostscript()` + `gsUnavailable()` (503 вместо
|
||
`spawn gs ENOENT`) для preview/compress/toImages, водяной знак с TTF и опциональными параметрами,
|
||
owner-check в `/download`, `crypto.randomBytes` в именах, лимиты размера/количества сняты для админа
|
||
- [routes/compress.js](../../routes/compress.js) — случайные суффиксы в именах архива и single-файла,
|
||
`owner.claim`, owner-check в `/compress/download`, лимитер через общую фабрику
|
||
- [server.js](../../server.js) — owner-check в `/download/:filename`
|
||
- [routes/api.js](../../routes/api.js), [routes/auth.js](../../routes/auth.js),
|
||
[routes/favicon.js](../../routes/favicon.js), [routes/redirects.js](../../routes/redirects.js),
|
||
[routes/parser.js](../../routes/parser.js), [routes/httpclient.js](../../routes/httpclient.js) —
|
||
переведены на `createLimiter` (в auth — `skipAdmin: false`)
|
||
- [public/pdf.html](../../public/pdf.html) — ветка `case 'watermark'` в `sendRequest()`, убрана
|
||
обязательность текста, метка «(необязательно)»
|
||
- [public/video.html](../../public/video.html) — детект админа вынесен на загрузку страницы
|
||
(раньше `/auth/me` спрашивался уже после проверки размера — первый большой файл всегда отклонялся),
|
||
`MAX_SIZE = Infinity` для админа
|
||
- [package.json](../../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`.
|
||
|
||
**Ссылки:**
|
||
- Прод: https://wadevelop.ru — VPS 195.140.146.84 (FirstVDS, treamz.fvds.ru), контейнер `wadevelop`
|
||
- Предыдущая сессия: [2026-07-24 23:48 — аудит безопасности](2026-07-24-2348.md)
|
||
- Git: локаль на коммите `a3fb5a2` + незакоммиченные правки этой сессии
|