mirror of
https://gitverse.ru/kpa39l/icq.git
synced 2026-09-29 01:50:10 +00:00
first_commit
This commit is contained in:
+20
@@ -0,0 +1,20 @@
|
||||
# Секреты и данные (НЕ в репозиторий)
|
||||
certs/
|
||||
data/
|
||||
logs/
|
||||
backups/
|
||||
|
||||
# Пароли и ключи в доке — если вдруг появятся
|
||||
*.key
|
||||
*.pem
|
||||
*.crt
|
||||
|
||||
# Локальное
|
||||
.docker/
|
||||
*.log
|
||||
|
||||
# Веб-клиент: полный dist Converse (24MB, скачивается из релиза)
|
||||
webchat/dist/
|
||||
# Легаси-бандлы в корне webchat (index.html использует /dist/*)
|
||||
webchat/converse.min.js
|
||||
webchat/converse.min.css
|
||||
@@ -0,0 +1,89 @@
|
||||
# Миграция XMPP: chat.nixg.ru → nixg.ru (юзеры user@nixg.ru, XMPP-домен через xmpp.nixg.ru)
|
||||
|
||||
> Дата: 2026-08-28 (вечер) · bigbox (/opt/icq)
|
||||
> Статус: ВЫПОЛНЕНО (кроме SRV-DNS у пользователя + замена самоподписанного cert)
|
||||
> Цель: JID юзеров `user@nixg.ru`, Prosody на VirtualHost nixg.ru, веб-клиент остаётся на https://chat.nixg.ru (страница),
|
||||
> WebSocket клиентов → **wss://xmpp.nixg.ru/xmpp-websocket** (поддомен, визитка на apex НЕ тронута).
|
||||
|
||||
## ВЫБРАННЫЙ ВАРИАНТ: xmpp.nixg.ru (а не apex!)
|
||||
- Пользователь согласился на вариант **xmpp.nixg.ru**: визитка на apex nixg.ru (81.177.135.175) осталась нетронутой.
|
||||
- XMPP-сервер: VirtualHost **nixg.ru** (JID: user@nixg.ru).
|
||||
- WS-адрес: **wss://xmpp.nixg.ru/xmpp-websocket** (A-запись xmpp.nixg.ru → 87.242.100.206, LE-серт через Caddy).
|
||||
- Веб-клиент: **https://chat.nixg.ru** (страница, Converse.js), websocket_url → wss://xmpp.nixg.ru/xmpp-websocket.
|
||||
|
||||
## Что сделано (все шаги выполнены и проверены)
|
||||
|
||||
### 1. Бэкап
|
||||
- /opt/icq/backups/pre-migration-20260828-184316.tar.gz (data, config, certs, docker-compose.yml, webchat/)
|
||||
- /opt/icq/backups/data-pre-migration-clean-*.tar.gz
|
||||
|
||||
### 2. DNS (панель Jino) — ЧАСТИЧНО, ОСТАЛОСЬ У ПОЛЬЗОВАТЕЛЯ
|
||||
- ✅ A `xmpp.nixg.ru` → 87.242.100.206 (добавлен, проверено через dig @8.8.8.8)
|
||||
- ⏳ SRV _xmpp-client._tcp.nixg.ru → **0 5 5222 xmpp.nixg.ru.** (сейчас ещё chat.nixg.ru)
|
||||
- ⏳ SRV _xmpp-server._tcp.nixg.ru → **0 5 5269 xmpp.nixg.ru.** (сейчас ещё chat.nixg.ru)
|
||||
- ⏳ SRV _xmpp-client-websocket._tcp.nixg.ru → **0 5 443 xmpp.nixg.ru.** (сейчас ещё chat.nixg.ru — заметил пользователь!)
|
||||
- НЕ ТРОГАТЬ: A nixg.ru (визитка 81.177.135.175), A chat.nixg.ru (веб), MX mail.nixg.ru (почта)
|
||||
|
||||
### 3. Сертификаты
|
||||
- ✅ Caddy на vps02 выпустил **настоящий Let's Encrypt** для **xmpp.nixg.ru** (acme-v02, issuer YE2, 2026-08-28, до 2026-11-26) — WSS работает.
|
||||
- ✅ Самоподписанный `nixg.ru.crt/key` сгенерирован (SAN: nixg.ru, xmpp.nixg.ru, localhost) → /opt/icq/certs/
|
||||
- ⏳ **TODO**: заменить самоподписанный cert Prosody (c2s/s2s 5222/5269) для nixg.ru на настоящий LE (dns-01) — для федерации.
|
||||
|
||||
### 4. Prosody (config/prosody.cfg.lua) — ✅
|
||||
- VirtualHost: **nixg.ru** (вместо chat.nixg.ru)
|
||||
- admins = { "admin@nixg.ru" }
|
||||
- ssl: nixg.ru.crt/key
|
||||
- cross_domain_websocket = { "https://chat.nixg.ru", "https://xmpp.nixg.ru" }
|
||||
- MUC: Component "conference.nixg.ru" "muc"
|
||||
- Аккаунт: **admin@nixg.ru**, SCRAM-хеш скопирован из data/chat%2enixg%2eru/accounts/admin.dat → data/nixg%2eru/accounts/admin.dat
|
||||
→ пароль тот же: `<в защищённом хранилище>`; проверено: вход по wss://xmpp.nixg.ru AUTH OK (slixmpp)
|
||||
- Старые данные chat%2enixg%2eru оставлены (для отката); PEP/OMEMO админа НЕ переносились (ключи пересоздаст клиент)
|
||||
|
||||
### 5. Caddy (vps02, /opt/caddy/Caddyfile) — ✅
|
||||
- Добавлен блок:
|
||||
```
|
||||
xmpp.nixg.ru {
|
||||
# XMPP WebSocket (Converse.js → nginx на bigbox, TLS выпустит Caddy)
|
||||
reverse_proxy 10.8.0.2:8081 {
|
||||
header_up Host {host}
|
||||
header_up X-Forwarded-Proto https
|
||||
}
|
||||
}
|
||||
```
|
||||
- Бэкап Caddyfile: /opt/caddy/Caddyfile.bak-*
|
||||
- Проверка: wss://xmpp.nixg.ru/xmpp-websocket → **101 Switching Protocols** (curl --http1.1, Sec-WebSocket-Protocol: xmpp)
|
||||
|
||||
### 6. nginx/webchat (bigbox) — ✅
|
||||
- webchat/index.html: websocket_url → **'wss://xmpp.nixg.ru/xmpp-websocket'**
|
||||
- webchat/nginx.conf: server_name chat.nixg.ru xmpp.nixg.ru
|
||||
|
||||
### 7. Проверка end-to-end — ✅
|
||||
- https://xmpp.nixg.ru/ → 200 (Caddy+nginx)
|
||||
- TLS: LE (issuer YE2), valid до 2026-11-26
|
||||
- WS: wss://xmpp.nixg.ru/xmpp-websocket → 101
|
||||
- Вход admin@nixg.ru через wss://xmpp.nixg.ru: AUTH OK (slixmpp)
|
||||
- Веб-чат: https://chat.nixg.ru открылся у пользователя (Converse.js работает)
|
||||
- ⚠️ ЗАЛИПШИЙ localStorage: в браузере мог остаться старый JID admin@chat.nixg.ru → ошибка host-unknown; просто удалить localStorage/зайти в инкогнито
|
||||
|
||||
## Что осталось
|
||||
1. **DNS (пользователь, панель Jino)**: 3 SRV-записи → xmpp.nixg.ru. (см. п.2)
|
||||
2. **Замена самоподписанного cert Prosody** на LE (dns-01) — для федерации (сейчас с s2s могут быть проблемы с другими серверами)
|
||||
3. **Документация**: WALKTHROUGH.md и PRD.md ещё содержат старые адреса chat.nixg.ru — обновить
|
||||
|
||||
## Как проверить (команды)
|
||||
```bash
|
||||
cd /opt/icq && docker compose ps
|
||||
dig +short A xmpp.nixg.ru # → 87.242.100.206
|
||||
dig +short SRV _xmpp-client._tcp.nixg.ru # → 0 5 5222 xmpp.nixg.ru.
|
||||
echo | openssl s_client -connect xmpp.nixg.ru:443 -servername xmpp.nixg.ru 2>/dev/null | openssl x509 -noout -issuer -dates
|
||||
curl -sk --http1.1 -v -H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Sec-WebSocket-Protocol: xmpp" https://xmpp.nixg.ru/xmpp-websocket # → 101
|
||||
python3 /tmp/wstest_nixg.py # AUTH OK admin@nixg.ru
|
||||
```
|
||||
|
||||
## Файлы
|
||||
- /opt/icq/config/prosody.cfg.lua
|
||||
- /opt/icq/docker-compose.yml
|
||||
- /opt/icq/webchat/index.html, /opt/icq/webchat/nginx.conf
|
||||
- /tmp/wstest_nixg.py (тест входа)
|
||||
- Caddy: /opt/caddy/Caddyfile (vps02)
|
||||
- DNS: панель Jino (nixg.ru)
|
||||
@@ -0,0 +1,180 @@
|
||||
# PRD: Семейный мессенджер ICQ (XMPP) на chat.nixg.ru
|
||||
|
||||
> Версия: 0.2 · Дата: 2026-08-28 · Статус: в разработке (Этап 1 завершён, Этап 2 начат)
|
||||
>
|
||||
> **Практический разбор всего сделанного: [WALKTHROUGH.md](WALKTHROUGH.md)** —
|
||||
> пошаговое описание с командами, почему выбрана такая архитектура, и все «засады».
|
||||
>
|
||||
> **Текущее состояние и план: [STATUS.md](STATUS.md)** — учётные данные, что работает,
|
||||
> проблема пустой страницы + план исправления (точка входа для новой сессии).
|
||||
|
||||
## 1. Обзор (Executive Summary)
|
||||
|
||||
Разворачиваем собственный федеративный мессенджер для семьи на базе протокола
|
||||
**XMPP**. Сервер (Prosody) живёт на **bigbox** в каталоге `/opt/icq`, наружу
|
||||
выставляется через **vps02** (Caddy + WireGuard). Домен: **chat.nixg.ru**.
|
||||
|
||||
Федерализация — обязательное требование: общение с любым XMPP-сервером в мире
|
||||
(email-подобная модель s2s).
|
||||
|
||||
Дополнительно: **бот-книгоискатель**, который по запросу находит и скачивает
|
||||
книги из сетевых библиотек (OPDS-каталоги) и присылает файл прямо в чат.
|
||||
|
||||
## 2. Цели (Goals)
|
||||
|
||||
- Личный, принадлежащий нам мессенджер для семьи (родители, дети).
|
||||
- Федеративность: связь с пользователями любых XMPP-серверов.
|
||||
- Клиенты под iOS и Android из официальных сторов (Snikket / Monal / Conversations).
|
||||
- Сквозное шифрование (OMEMO) по умолчанию.
|
||||
- Работает через наш туннель и не зависит от публичных мессенджеров.
|
||||
- Бот, автоматизирующий скачивание книг из сетевых библиотек.
|
||||
|
||||
## 3. Не-цели (Non-Goals)
|
||||
|
||||
- Не пытаемся «слямзить» протокол Telegram/MTProto — это закрытый централизованный
|
||||
протокол, серверная часть не существует и не появится. Федеративность = XMPP.
|
||||
- Не строим свой клиент — используем готовые (Element/Snikket/Monal/Conversations).
|
||||
- Пока не делаем мост в Telegram/WhatsApp (mautrix) — только если понадобится позже.
|
||||
|
||||
## 4. Архитектура
|
||||
|
||||
### 4.1 Компоненты
|
||||
|
||||
```
|
||||
[Клиенты: iOS/Android (Snikket/Monal/Conversations)]
|
||||
│ c2s (5222, TLS)
|
||||
▼
|
||||
[bigbox] Prosody (XMPP-сервер, Docker, /opt/icq)
|
||||
│ s2s (5269, TLS) — федерация с другими серверами
|
||||
│ http_upload (XEP-0363) — передача файлов (книги бота)
|
||||
▼
|
||||
[Бот-книгоискатель] slixmpp на bigbox, логинится как bot@chat.nixg.ru
|
||||
│ OPDS (HTTP)
|
||||
▼
|
||||
[Сетевые библиотеки: Флибуста и др.]
|
||||
```
|
||||
|
||||
### 4.2 Сеть (доступность)
|
||||
|
||||
- **bigbox** — внутренний сервер (без публичного IP), Docker Compose.
|
||||
- **vps02** — публичный вход: Caddy (TLS, reverse-proxy WebSocket/HTTP) + WireGuard
|
||||
туннель до bigbox.
|
||||
- **Порты:** 443 (клиенты, веб) + 5222 (прямые клиенты) + 5269 (федерация s2s).
|
||||
- Форвард 5222/5269 с vps02 на bigbox через WireGuard (socat или iptables).
|
||||
|
||||
### 4.3 DNS (важно для федерации)
|
||||
|
||||
```
|
||||
A chat.nixg.ru → публичный IP vps02
|
||||
SRV _xmpp-client._tcp.chat.nixg.ru → chat.nixg.ru:5222
|
||||
SRV _xmpp-server._tcp.chat.nixg.ru → chat.nixg.ru:5269
|
||||
```
|
||||
|
||||
Без SRV-записей другие XMPP-серверы не смогут нас найти (федерация не заработает).
|
||||
Входящая федерация требует открытого 5269 на vps02.
|
||||
|
||||
### 4.4 TLS / сертификаты
|
||||
|
||||
- 443: Caddy выпускает сам (Let's Encrypt, http-01).
|
||||
- 5222/5269: dns-01 (домен у нас, DNS контролируем) → e.g. через Caddy/acme.sh.
|
||||
- Сертификат должен покрывать имена `chat.nixg.ru` (+ возможно `conference.chat.nixg.ru` для MUC).
|
||||
|
||||
## 5. Стек технологий
|
||||
|
||||
| Слой | Технология | Почему |
|
||||
|------|-----------|--------|
|
||||
| Протокол | XMPP (RFC 6120/6121) | Федеративный, открытый, XML — как email |
|
||||
| Сервер | Prosody (Lua, Docker) | Лёгкий, гибкий, модули (http_upload, muc), удобен за реверс-прокси |
|
||||
| Внешний доступ | Caddy на vps02 + WireGuard | TLS автоматически, reverse-proxy, WG-туннель до bigbox |
|
||||
| Клиенты iOS | Monal / Snikket | Федерация + OMEMO + push (APNs) |
|
||||
| Клиенты Android | Conversations / Snikket | Федерация + OMEMO + push (FCM) |
|
||||
| Бот | Python + slixmpp | Зрелая живая библиотека XMPP |
|
||||
| Поиск книг | OPDS (Atom/XML каталоги) | Стандарт сетевых библиотек (Флибуста и др.) |
|
||||
| Передача файлов | XMPP HTTP Upload (XEP-0363) | Отправка книг в чат |
|
||||
| Развёртывание | Docker Compose в /opt/icq | Изоляция, простота, переносимость |
|
||||
|
||||
Примечание: Snikket как *сервер* не берём (спроектирован под прямой 443, капризен
|
||||
за реверс-прокси), но его *клиенты* хороши — используем их.
|
||||
|
||||
## 6. Функциональные требования
|
||||
|
||||
### 6.1 Обязательные (Must have)
|
||||
- FR-1: Регистрация/вход по JID (user@chat.nixg.ru) с паролем.
|
||||
- FR-2: Личные сообщения 1-на-1, статусы присутствия (presence).
|
||||
- FR-3: Групповые чаты (MUC, конференции).
|
||||
- FR-4: Сквозное шифрование OMEMO.
|
||||
- FR-5: Передача файлов (HTTP Upload) — фото, документы.
|
||||
- FR-6: Федерация s2s — общение с пользователями других серверов.
|
||||
- FR-7: Админка/панель управления сервером (Prosody admin + командная строка).
|
||||
- FR-8: Клиенты iOS и Android из официальных сторов.
|
||||
|
||||
### 6.2 Желательные (Should have)
|
||||
- FR-9: Push-уведомления на iOS/Android (APNs/FCM) — через push-шлюз (Snikket/Conversations могут ходить на свой или публичный).
|
||||
- FR-10: Бот-книгоискатель (slixmpp + OPDS).
|
||||
- FR-11: Бэкапы базы/конфигов (в наш общий бэкап /opt/hermes).
|
||||
- FR-12: Логирование и мониторинг (доступность).
|
||||
|
||||
### 6.3 Мосты в Telegram и WhatsApp (Bridges)
|
||||
- FR-13: Мост в **Telegram** (mautrix-telegram) — общение с контактами Telegram из нашего клиента.
|
||||
- Двусторонняя переписка: наш пользователь ↔ контакт в Telegram.
|
||||
- Возможность отправлять/принимать файлы, реакции (по возможностям моста).
|
||||
- Авторизация через Telegram (логин/QR/MTProto), управление через панель бота моста.
|
||||
- FR-14: Мост в **WhatsApp** (mautrix-whatsapp) — общение с контактами WhatsApp из нашего клиента.
|
||||
- Двусторонняя переписка: наш пользователь ↔ контакт в WhatsApp.
|
||||
- Авторизация через QR-код WhatsApp Web.
|
||||
- Передача файлов, голосовых (по возможностям моста).
|
||||
- FR-15: Общий мост-хостинг (mautrix с Postgres/базой), регистрация в Prosody как компоненты.
|
||||
- OQ-5: Какие мосты деплоим сразу (оба), какие позже? Сначала Telegram, потом WhatsApp (обычно надёжнее).
|
||||
|
||||
## 7. Бот-книгоискатель (детально)
|
||||
|
||||
### 7.1 Сценарий
|
||||
1. Пользователь пишет боту `book@chat.nixg.ru`: «Пушкин Капитанская дочка».
|
||||
2. Бот ищет по OPDS-каталогам (Флибуста и др.).
|
||||
3. Находит, скачивает (epub/fb2), присылает файлом через HTTP Upload.
|
||||
4. (Опц.) Ограничение размера, только легальные источники.
|
||||
|
||||
### 7.2 Требования
|
||||
- FR-B1: Приём запросов через личные сообщения.
|
||||
- FR-B2: Поиск по OPDS (Atom feed).
|
||||
- FR-B3: Скачивание и отправка файла в чат (XEP-0363).
|
||||
- FR-B4: Логирование запросов.
|
||||
- FR-B5: Анти-флуд / ограничения.
|
||||
|
||||
## 8. Открытые вопросы (Open Questions)
|
||||
|
||||
- OQ-1: Какая библиотека OPDS основная? Флибуста (нужны ли регистрация/логин)?
|
||||
- OQ-2: ~~Snikket-клиенты работают только со Snikket-серверами?~~ (нет — возьмём Monal + Conversations)
|
||||
- OQ-3: Нужен ли мост Telegram (mautrix-telegram) сразу или позже?
|
||||
- OQ-4: ~~Нужен ли веб-клиент (Converse.js) на 443 через Caddy?~~ **✅ СДЕЛАНО 2026-08-28** —
|
||||
https://chat.nixg.ru, Converse.js v14, локальные ассеты, WebSocket wss://chat.nixg.ru/xmpp-websocket.
|
||||
|
||||
## 9. Roadmap (этапы)
|
||||
|
||||
1. **Этап 1 — Фундамент (✅ ЗАВЕРШЁН 2026-08-28):** Docker Compose Prosody на bigbox (/opt/icq),
|
||||
базовые модули, DNS на chat.nixg.ru + SRV (A, _xmpp-client, _xmpp-server — внесены на Jino),
|
||||
Caddy на vps02 (443, reverse-proxy), форвард 5222/5269 через WG (iptables DNAT + FORWARD + MASQUERADE),
|
||||
группа безопасности Timeweb (TCP 5222/5269), веб-клиент Converse.js v14 на 443.
|
||||
Проверено: 5222/5269 открыты извне, webchat отдаётся, WebSocket 101 Switching Protocols.
|
||||
Подробности: [WALKTHROUGH.md](WALKTHROUGH.md).
|
||||
2. **Этап 2 — Семья и федерация:** MUC (есть), OMEMO, **HTTP Upload (XEP-0363)** — установить
|
||||
community-модуль mod_http_upload из prosody-modules (в docker-образе Prosody 0.11.9 его НЕТ),
|
||||
добавление внешнего контакта (проверка s2s), настоящие сертификаты dns-01 для 5222/5269.
|
||||
Приглашение семьи. Клиенты iOS/Android.
|
||||
3. **Этап 3 — Бот:** slixmpp-бот с OPDS-поиском, HTTP Upload.
|
||||
4. **Этап 4 — Полировка:** push, бэкапы, мониторинг, (опц.) мосты mautrix.
|
||||
|
||||
## 10. Принятые решения (Decisions)
|
||||
|
||||
- D-1: Протокол = XMPP (не Telegram/MTProto).
|
||||
- D-2: Сервер = Prosody (не Snikket-сервер, не Ejabberd).
|
||||
- D-3: Размещение = bigbox (/opt/icq), внешний вход = vps02 (Caddy + WG).
|
||||
- D-4: Домен = chat.nixg.ru.
|
||||
- D-5: Бот = Python slixmpp + OPDS + HTTP Upload.
|
||||
|
||||
## 11. Риски
|
||||
|
||||
- R-1: Флибуста меняет зеркала/требует VPN/логин — бот должен уметь переключать источники.
|
||||
- R-2: Push на iOS (APNs) сложен для self-hosted — упираемся в публичный push-шлюз.
|
||||
- R-3: Ограничение 5269 у провайдера vps02 — проверить заранее.
|
||||
- R-4: Семейные пользователи малознакомы с XMPP-клиентами — подготовить инструкцию.
|
||||
@@ -0,0 +1,50 @@
|
||||
# ICQ — приватный XMPP-сервер (Prosody + Converse.js)
|
||||
|
||||
Личный XMPP-сервер для семьи/друзей. Домен: **nixg.ru**, веб-чат: **https://chat.nixg.ru**.
|
||||
|
||||
## Стек
|
||||
- **Prosody 0.11.9** (Docker) — XMPP-сервер: c2s 5222, s2s 5269, WebSocket 5280, HTTPS 5281
|
||||
- **Converse.js v14** (веб-клиент, ESM) — отдаётся nginx (Docker), доступ через Caddy (vps02)
|
||||
- **Caddy** (vps02, host-network) — TLS-терминатор: chat.nixg.ru, xmpp.nixg.ru, upload.nixg.ru
|
||||
- **Let's Encrypt** — сертификаты для XMPP (c2s/s2s) и веб
|
||||
- **WireGuard** — туннель bigbox (10.8.0.2) ↔ vps02 (10.8.0.4)
|
||||
|
||||
## Состав (после клонирования)
|
||||
```
|
||||
/opt/icq
|
||||
├── config/ # prosody.cfg.lua (+ mounts в контейнер)
|
||||
├── webchat/ # nginx: index.html + nginx.conf (dist скачивается отдельно)
|
||||
├── modules/ # community-модули Prosody (mod_http_upload)
|
||||
├── docker-compose.yml
|
||||
├── STATUS.md # точка входа: текущее состояние, задачи
|
||||
├── PRD.md # требования/архитектура
|
||||
├── MIGRATION.md # переезд chat.nixg.ru → nixg.ru
|
||||
└── WALKTHROUGH.md # команды, засады, решения
|
||||
```
|
||||
|
||||
## Быстрый запуск
|
||||
```bash
|
||||
# 1. dist Converse.js (в .gitignore, т.к. 24MB) — скачать из релиза:
|
||||
# https://github.com/conversejs/converse/releases → converse.js-14.0.0.tgz
|
||||
# распаковать package/dist/* в webchat/dist/
|
||||
|
||||
# 2. Сертификаты: положить LE-серт nixg.ru (fullchain, privkey) в certs/nixg.ru.{crt,key}
|
||||
|
||||
# 3. Запуск
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
## Домены (DNS, Jino)
|
||||
| Запись | Значение |
|
||||
|---|---|
|
||||
| A chat.nixg.ru, xmpp.nixg.ru, upload.nixg.ru | 87.242.100.206 (vps02) |
|
||||
| SRV _xmpp-client._tcp.nixg.ru | 5222 xmpp.nixg.ru |
|
||||
| SRV _xmpp-server._tcp.nixg.ru | 5269 xmpp.nixg.ru |
|
||||
| SRV _xmpp-client-websocket._tcp.nixg.ru | 443 xmpp.nixg.ru |
|
||||
| TXT _acme-challenge.nixg.ru (при продлении) | временно |
|
||||
|
||||
## Лицензия
|
||||
Приватный проект. Пароли и ключи в репозиторий не попадают (см. .gitignore).
|
||||
|
||||
---
|
||||
Подробности: [STATUS.md](STATUS.md) (текущее состояние), [PRD.md](PRD.md), [WALKTHROUGH.md](WALKTHROUGH.md)
|
||||
@@ -0,0 +1,133 @@
|
||||
# ICQ XMPP — ТЕКУЩИЙ СТАТУС И ПЛАН (точка входа для новой сессии)
|
||||
|
||||
> Обновлено: 2026-08-28 ≈ 19:00 UTC · Сессия: bigbox (/opt/icq)
|
||||
> ⚠️ МИГРАЦИЯ НА nixg.ru (2026-08-28): JID юзеров теперь `user@nixg.ru`, веб-клиент остаётся на chat.nixg.ru, WS → wss://xmpp.nixg.ru. См. MIGRATION.md.
|
||||
> Читать вместе с: [PRD.md](PRD.md) (требования/архитектура), [WALKTHROUGH.md](WALKTHROUGH.md) (опыт, команды, засады), [MIGRATION.md](MIGRATION.md) (переезд).
|
||||
|
||||
----------------------------------------------------------------------
|
||||
## 1. КРИТИЧНЫЕ ДАННЫЕ (для продолжения работы)
|
||||
|
||||
### Учётные данные
|
||||
- **admin@nixg.ru / пароль: `<СМОТРИ В ЗАЩИЩЁННОМ ХРАНИЛИЩЕ / .env>`** ← перенесён 2026-08-28 с admin@chat.nixg.ru, ВХОД ПОДТВЕРЖДЁН
|
||||
(проверено через WebSocket: slixmpp + wss://xmpp.nixg.ru/xmpp-websocket → AUTH OK).
|
||||
- Если пароль снова понадобится сбросить:
|
||||
`docker exec icq-prosody prosodyctl register admin nixg.ru 'НОВЫЙПАРОЛЬ'`
|
||||
- Старый аккаунт admin@chat.nixg.ru: домен chat.nixg.ru убран из Prosody (VirtualHost теперь nixg.ru).
|
||||
|
||||
### Инфраструктура (после миграции)
|
||||
- bigbox (машина агента), каталог: **/opt/icq**
|
||||
- Контейнеры: **icq-prosody** (Prosody 0.11.9: 5222/5269/5280/5281), **icq-webchat** (nginx:8081)
|
||||
- vps02 (87.242.100.206, Debian 12, enp3s0): Caddy (443, host-network), iptables DNAT 5222/5269 → 10.8.0.2
|
||||
- WireGuard: bigbox 10.8.0.2 ↔ vps02 10.8.0.4; Caddy → 10.8.0.2:8081
|
||||
- DNS (Jino):
|
||||
- A `chat.nixg.ru` → 87.242.100.206 (веб-клиент, остаётся)
|
||||
- A `xmpp.nixg.ru` → 87.242.100.206 (XMPP WS, **добавлен**)
|
||||
- A `nixg.ru` → 81.177.135.175 (визитка, НЕ трогаем)
|
||||
- SRV `_xmpp-client._tcp.nixg.ru` → 0 5 5222 **xmpp.nixg.ru.** (было chat.nixg.ru)
|
||||
- SRV `_xmpp-server._tcp.nixg.ru` → 0 5 5269 **xmpp.nixg.ru.** (было chat.nixg.ru)
|
||||
- SRV `_xmpp-client-websocket._tcp.nixg.ru` → 0 5 443 **xmpp.nixg.ru.** (было chat.nixg.ru)
|
||||
- MX `mail.nixg.ru` — НЕ трогаем (почта)
|
||||
- Prosody: VirtualHost **nixg.ru**, cross_domain_websocket = { "https://chat.nixg.ru", "https://xmpp.nixg.ru" }
|
||||
- WebSocket: wss://xmpp.nixg.ru/xmpp-websocket — РАБОТАЕТ (101, авторизация OK, LE-сертификат)
|
||||
|
||||
### Что уже РАБОТАЕТ (подтверждено)
|
||||
- ✅ XMPP-сервер на nixg.ru, федерация (SRV), порты 5222/5269 открыты извне
|
||||
- ✅ TLS на 443 (Caddy/Let's Encrypt): xmpp.nixg.ru (LE, получен 2026-08-28) и chat.nixg.ru (страница)
|
||||
- ✅ WebSocket-путь целиком: Caddy (xmpp.nixg.ru:443 LE) → nginx:8081 → Prosody:5280 → 101 Switching Protocols
|
||||
- ✅ Аутентификация по WebSocket: admin@nixg.ru входит через wss://xmpp.nixg.ru (проверено slixmpp, 2026-08-28)
|
||||
- ✅ **ВЕБ-ЧАТ https://chat.nixg.ru/ РАБОТАЕТ** (v14.0.0, full dist, ru; Converse подключается к
|
||||
wss://xmpp.nixg.ru, вход admin@nixg.ru подтверждён headless-браузером)
|
||||
|
||||
## 2. ТЕКУЩАЯ ПРОБЛЕМА (блокер)
|
||||
**ПУСТАЯ СТРАНИЦА — РЕШЕНО ✅ (2026-08-28 ≈ 18:30 UTC)**
|
||||
|
||||
### КОРЕНЬ ПРИЧИНЫ (диагностировано headless-браузером)
|
||||
Converse **v14 собирается как ES-модуль** (ESM), а НЕ как классический UMD-бандл:
|
||||
- бандл использует `import.meta.url` (webpack auto-publicPath) и `export{c as default}`;
|
||||
- подключение через обычный `<script src="converse.min.js">` даёт в Chrome/Firefox:
|
||||
`Uncaught SyntaxError: Cannot use 'import.meta' outside a module` → скрипт не выполняется → пустая страница.
|
||||
- Ранее лежавший в корне `/converse.min.js` (1.3MB, скачан с CDN) — это и был ESM-бандл, но без `type="module"` он молча падал; также отсутствовал `/dist/` с чанками.
|
||||
|
||||
### РЕШЕНИЕ (Шаги 2+3 плана, выполнено)
|
||||
1. Скачан официальный релиз `converse.js-14.0.0.tgz` (GitHub) и full dist разложен в `/opt/icq/webchat/dist/`:
|
||||
`converse.min.js`, `converse.min.css`, `chunkjs/locales/...` (русская локаль), `libomemo.esm.min.js`, `curve25519_compiled.wasm`, sounds, webfonts, images.
|
||||
2. `/opt/icq/webchat/index.html` переписан: `<script type="module" src="/dist/converse.min.js">`
|
||||
+ `import converse from '/dist/converse.min.js'` + `converse.initialize({...})`.
|
||||
nginx: контейнер монтирует ./webchat:/usr/share/nginx/html — подхватилось без рестарта.
|
||||
3. Проверено headless-браузером (chromium --headless + Camofox): UI рендерится (лог-ин форма,
|
||||
русская локаль), вход admin@chat.nixg.ru проходит, XMPP-сессия идёт через wss://chat.nixg.ru/xmpp-websocket.
|
||||
|
||||
### Остаточные мелочи (не блокеры)
|
||||
- ✅ host-meta (XEP-0156) ИСПРАВЛЕН (2026-08-28): nginx webchat отдаёт JSON на /.well-known/host-meta и /.well-known/host-meta.json → wss://xmpp.nixg.ru/xmpp-websocket. Ошибка «XML Parsing Error» в консоли Converse ушла.
|
||||
- Ошибка MAM `<iq ... max_items: not a number>` — запрос архива с нечисловым max_items;
|
||||
появляется при логине, не мешает чату. Проверить настройку MAM (mod_mam) в Prosody 0.11.9.
|
||||
- ⚠️ ЗАЛИПШИЙ localStorage браузера: мог остаться старый JID `admin@chat.nixg.ru` → Converse
|
||||
шлёт host-unknown (Prosody не знает chat.nixg.ru). Лечится: localStorage.removeItem("conversejs-session-jid")
|
||||
или вход в инкогнито. На новом JID admin@nixg.ru работает.
|
||||
- SRV _xmpp-client-websocket._tcp.nixg.ru всё ещё → chat.nixg.ru (надо → xmpp.nixg.ru.) — TODO у пользователя.
|
||||
|
||||
### История гипотез (для справки — была неверна)
|
||||
- H1 (assets_path=/dist), H2 (опции initialize), H3 (способ подключения) — НЕ были причиной;
|
||||
реальная причина — ESM-природа v14 + отсутствие type="module" и полного dist.
|
||||
|
||||
## 3. КАК ЭТО БЫЛО ИСПРАВЛЕНО (команды, реально сработавшие)
|
||||
|
||||
```bash
|
||||
# 1. Скачать официальный релиз v14 (в нём ПОЛНАЯ dist-структура с чанками)
|
||||
cd /tmp && curl -sL -o converse-14.0.0.tgz \
|
||||
https://github.com/conversejs/converse/releases/download/v14.0.0/converse.js-14.0.0.tgz
|
||||
mkdir -p converse-extract && tar xzf converse-14.0.0.tgz -C converse-extract
|
||||
mkdir -p /opt/icq/webchat/dist && cp -r converse-extract/package/dist/* /opt/icq/webchat/dist/
|
||||
|
||||
# 2. index.html: подключить КАК ES-МОДУЛЬ (ключ!):
|
||||
# <script type="module" src="/dist/converse.min.js"></script>
|
||||
# <script type="module">
|
||||
# import converse from '/dist/converse.min.js';
|
||||
# converse.initialize({ websocket_url: 'wss://chat.nixg.ru/xmpp-websocket', view_mode: 'fullscreen', auto_login: false, theme: 'concord', i18n: 'ru', show_controlbox_by_default: true });
|
||||
# </script>
|
||||
|
||||
# 3. Проверка headless (без установки новых пакетов — chromium уже в snap):
|
||||
chromium --headless --no-sandbox --disable-gpu --virtual-time-budget=15000 --dump-dom https://chat.nixg.ru/ | grep converse-login-form
|
||||
# → если есть converse-login-form — UI отрендерен
|
||||
# Реальный вход: camofox-browser open https://chat.nixg.ru/ + eval (см. ниже)
|
||||
```
|
||||
|
||||
Диагностика console-ошибок headless:
|
||||
```bash
|
||||
# до фикса давало: Uncaught SyntaxError: Cannot use 'import.meta' outside a module
|
||||
chromium --headless --no-sandbox --disable-gpu --enable-logging=stderr --virtual-time-budget=20000 --dump-dom https://chat.nixg.ru/ 2>&1 | grep -iE 'CONSOLE|Uncaught'
|
||||
# удобнее: camofox-browser (npm -g) → open/eval/console — см. /tmp/icu-* в WALKTHROUGH
|
||||
```
|
||||
|
||||
## 4. ДАЛЬНЕЙШИЕ ЗАДАЧИ (после фикса страницы)
|
||||
- [x] Обновить SRV в панели Jino: _xmpp-client/_xmpp-server/_xmpp-client-websocket._tcp.nixg.ru → xmpp.nixg.ru. ✅ (2026-08-28, подтверждено dig @1.1.1.1)
|
||||
- [x] **Заменить самоподписанный cert Prosody (c2s/s2s 5222/5269) для nixg.ru на настоящий LE — ВЫПОЛНЕНО ✅ (2026-08-28)**
|
||||
- certbot 2.9.0 (apt) на bigbox; выпуск: `sudo certbot certonly --manual --preferred-challenges dns -d nixg.ru -d xmpp.nixg.ru --manual-auth-hook /bin/true --manual-cleanup-hook /bin/true`
|
||||
- TXT-записи `_acme-challenge.nixg.ru` и `_acme-challenge.xmpp.nixg.ru` в панели Jino (вручную)
|
||||
- Сертификат: /etc/letsencrypt/live/nixg.ru/ → скопирован в /opt/icq/certs/nixg.ru.{crt,key}
|
||||
- Проверено снаружи: c2s 5222 и s2s 5269 отдают LE (issuer=Let's Encrypt, SAN nixg.ru+xmpp.nixg.ru), TLS 1.3
|
||||
- Истекает 2026-11-26; certbot настроил автопродление — НО при dns-01 вручную автопродление НЕ сработает без повторного добавления TXT! Продление: `sudo certbot renew --manual-auth-hook /bin/true --manual-cleanup-hook /bin/true` (после добавления TXT). TODO: настроить напоминание/скрипт.
|
||||
- [ ] HTTP Upload (XEP-0363): установить mod_http_upload из prosody-modules
|
||||
(в docker Prosody 0.11.9 модуля НЕТ). Путь модулей: ./modules в compose.
|
||||
Подробности в WALKTHROUGH.md раздел 7.
|
||||
- [ ] OMEMO (сквозное шифрование) — Проверить включение модуля, клиенты.
|
||||
- [ ] Бот-книгоискатель (slixmpp + OPDS) — Этап 3 PRD.
|
||||
- [ ] Мосты mautrix (Telegram/WhatsApp) — Этап 4; нужен external component (mod_component).
|
||||
- [ ] Push-уведомления (APNs/FCM).
|
||||
|
||||
## 5. Полезные команды (кратко)
|
||||
```bash
|
||||
cd /opt/icq && docker compose ps
|
||||
docker logs icq-prosody --tail 50
|
||||
docker logs icq-webchat --since 5m
|
||||
docker exec icq-prosody prosodyctl check config
|
||||
docker exec icq-prosody prosodyctl register admin nixg.ru 'ПАРОЛЬ'
|
||||
# проверка WS рукоятки (python, см. прошлые сессии /tmp/wstest)
|
||||
```
|
||||
Проверка входа по WebSocket (slixmpp, ssl не проверяется):
|
||||
```python
|
||||
bot = slixmpp.ClientXMPP('admin@nixg.ru','ПАРОЛЬ')
|
||||
import ssl; ctx = ssl.create_default_context(); ctx.check_hostname=False; ctx.verify_mode=ssl.CERT_NONE
|
||||
bot.ssl_context = ctx
|
||||
bot.connect(('wss://xmpp.nixg.ru/xmpp-websocket',))
|
||||
```
|
||||
+169
@@ -0,0 +1,169 @@
|
||||
# Развёртывание XMPP-мессенджера ICQ на chat.nixg.ru — Полный разбор
|
||||
|
||||
> Итоговый документ опыта. Дата: 2026-08-28.
|
||||
> Команда: `bigbox` (внутренний сервер, /opt/icq) + `vps02` (публичный вход, Caddy + WireGuard).
|
||||
> Домен: **chat.nixg.ru** · Протокол: **XMPP** · Сервер: **Prosody 0.11.9** (Docker).
|
||||
|
||||
----------------------------------------------------------------------
|
||||
## 1. Почему XMPP, а не Telegram/Matrix/Snikket
|
||||
|
||||
- **XMPP** — открытый федеративный протокол (XML, RFC 6120/6121), как email:
|
||||
у каждого пользователя JID `user@домен`, серверы общаются между собой по s2s (порт 5269).
|
||||
Главное требование семьи — «общаться со всеми» — выполняется федерацией.
|
||||
- **Telegram** — закрытый централизованный протокол (MTProto, бинарный). Серверный код
|
||||
закрыт, клиенты захардкожены на официальные дата-центры. «Слямзить» протокол невозможно.
|
||||
Bot API — только HTTP+JSON поверх MTProto, федерации нет.
|
||||
- **Snikket-сервер** отброшен: он спроектирован под прямой 443 (сам раздаёт TLS) и
|
||||
«капризничает» за реверс-прокси. Его КЛИЕНТЫ (iOS/Android) — отличные, используем их.
|
||||
- **Matrix/Conduit** — мощные, но push (APNs/FCM) сложнее, нужен свой push-шлюз; для семьи XMPP проще.
|
||||
|
||||
## 2. Архитектура (почему так)
|
||||
|
||||
```
|
||||
[Клиенты iOS/Android] ← c2s:5222 (TLS), wss:443 (веб)
|
||||
│
|
||||
▼
|
||||
[vps02: 87.242.100.206] ← публичный IP, Caddy (TLS/443) + iptables DNAT (5222/5269)
|
||||
│ WireGuard-туннель: bigbox=10.8.0.2, vps02=10.8.0.4
|
||||
▼
|
||||
[bigbox: 10.8.0.2] ← внутри, /opt/icq, Docker Compose
|
||||
├─ icq-prosody — Prosody 0.11.9 (5222, 5269, 5280)
|
||||
└─ icq-webchat — nginx (8081): статика Converse.js + прокси WebSocket на Prosody
|
||||
```
|
||||
|
||||
- **bigbox не имеет публичного IP** — весь внешний трафик идёт через vps02.
|
||||
- **Caddy** сам выпускает Let's Encrypt для 443 и проксирует WebSocket/HTTP.
|
||||
- **5222/5269** (XMPP-клиенты и федерация) — DNAT в iptables на vps02 → 10.8.0.2.
|
||||
- Порт **5280** (Prosody HTTP: WebSocket/BOSH) наружу НЕ торчит — веб-клиент ходит через
|
||||
Caddy на 443 (wss://), а боты/внутренние сервисы — напрямую.
|
||||
|
||||
## 3. Что сделано по шагам (с командами)
|
||||
|
||||
### 3.1 DNS (Jino, панель домена nixg.ru)
|
||||
| Запись | Тип | Значение |
|
||||
|--------|-----|----------|
|
||||
| `chat.nixg.ru` | A | `87.242.100.206` |
|
||||
| `_xmpp-client._tcp.nixg.ru` | SRV | `0 5 5222 chat.nixg.ru` |
|
||||
| `_xmpp-server._tcp.nixg.ru` | SRV | `0 5 5269 chat.nixg.ru` |
|
||||
|
||||
Проверка: `host -t SRV _xmpp-client._tcp.nixg.ru`
|
||||
|
||||
**Засада Jino**: панель склеивает поля в одну строку (`_xmpp-client._tcp.nixg.ruIN SRV0 5 5222 chat.nixg.ru`).
|
||||
Решение: заполнять поля формы по отдельности: имя=`_xmpp-client._tcp`, приоритет=`0`, вес=`5`, порт=`5222`, цель=`chat.nixg.ru.` (FQDN с точкой). Результат в DNS всё равно корректный.
|
||||
|
||||
SRV-записи критичны для федерации: без них другие XMPP-серверы не найдут нас.
|
||||
|
||||
### 3.2 Prosody на bigbox (/opt/icq)
|
||||
- `docker-compose.yml` — сервис `prosody` (prosody/prosody:latest), порты 5222/5269/5280/5281,
|
||||
volumes: ./data, ./config, ./certs, ./modules, ./logs.
|
||||
- Конфиг `config/prosody.cfg.lua`:
|
||||
- VirtualHost `chat.nixg.ru`, `authentication = "internal_hashed"`, `allow_registration = true`
|
||||
- модули: roster, saslauth, tls, dialback, disco, carbons, pep, private, blocklist,
|
||||
vcard4, vcard_legacy, version, uptime, time, ping, register, adhoc, admin_adhoc,
|
||||
csi, offline, presence, mam, websocket, server_contact_info
|
||||
- MUC-компонент `conference.chat.nixg.ru`
|
||||
- `cross_domain_websocket = { "https://chat.nixg.ru" }` — разрешить WebSocket с нашего домена
|
||||
- Логи — в файлы (не консоль), т.к. docker-образ по умолчанию пишет в файлы.
|
||||
- `https_ports` по умолчанию 5281 — даёт ошибку «No certificate present» (нет HTTPS-серта на 5281), не мешает.
|
||||
- Регистрация админа: `docker exec icq-prosody prosodyctl register admin chat.nixg.ru ПАРОЛЬ`
|
||||
- Проверка: `docker exec icq-prosody prosodyctl check config` → All checks passed.
|
||||
|
||||
**Тонкости Prosody 0.11.9 в docker-образе**:
|
||||
- Нет community-модулей: `http_upload` (XEP-0363), `smacks`, `s2s_bidi`, `xmpp_component`.
|
||||
Их нет в `/usr/lib/prosody/modules/`. Для http_upload нужна установка prosody-modules (см. раздел 7).
|
||||
- Порты 5222/5269/5280 слушаются на 0.0.0.0 — трафик снаружи приходит через WG.
|
||||
|
||||
### 3.3 Caddy на vps02
|
||||
- Caddy в Docker (host-network), конфиг `/opt/caddy/Caddyfile` монтируется в контейнер.
|
||||
- Блок для chat.nixg.ru: `reverse_proxy 10.8.0.2:8081` (веб-клиент+WS через nginx на bigbox).
|
||||
- **Засада с bind-mount**: при перезаписи Caddyfile через `sudo tee`/`>` файл получает НОВЫЙ inode,
|
||||
а монтированный в контейнер старый inode остаётся — Caddy продолжает читать СТАРУЮ версию.
|
||||
Симптом: «конфиг изменил, а Caddy проксирует на старый адрес». Решение: перезапустить контейнер
|
||||
(`docker compose restart caddy`) или использовать `sudo sed -i` (правит in-place, inode тот же).
|
||||
- Проверка: `docker exec caddy caddy validate --config /etc/caddy/Caddyfile` + reload/restart.
|
||||
|
||||
### 3.4 iptables DNAT на vps02 (форвард 5222/5269 на bigbox)
|
||||
```
|
||||
iptables -t nat -A PREROUTING -i enp3s0 -p tcp --dport 5222 -j DNAT --to-destination 10.8.0.2:5222
|
||||
iptables -t nat -A PREROUTING -i enp3s0 -p tcp --dport 5269 -j DNAT --to 10..0.2:5269
|
||||
iptables -A FORWARD -d 10.8.0.2 -p tcp --dport 5222 -j ACCEPT (и 5269)
|
||||
iptables -t nat -A POSTROUTING -o wg0 -j MASQUERADE
|
||||
```
|
||||
- **Обязательно добавить** FORWARD ACCEPT (default policy DROP!) и MASQUERADE — иначе пакеты
|
||||
докодят до bigbox, но ответы не возвращаются (connection refused).
|
||||
- Сохранение: `sudo sh -c "iptables-save > /etc/iptables/rules.v4"` (переживает reboot).
|
||||
- Группа безопасности у провайдера (Timeweb): открыть TCP 5222, TCP 5269 (только TCP).
|
||||
|
||||
### 3.5 Веб-клиент (Converse.js v14 + nginx)
|
||||
- `/opt/icq/webchat`: `index.html`, `converse.min.js` (1.3MB, v14.0.0), `converse.min.css` (545KB) — скачаны локально с CDN (чтобы не зависеть от cdn.conversejs.org — может резаться из РФ).
|
||||
- `ngnix.conf`: `server 8081`, root `/usr/share/nginx/html`; `location /xmpp-websocket { proxy_pass http://prosody:5280/xmpp-websocket; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }`; `location / { try_files $uri $uri/ /index.html; }`.
|
||||
- Docker-compose: сервис `webchat` (nginx:alpine), ports `8081:8081`, зависит от prosody.
|
||||
|
||||
**Критическая засада веб-клиента**: в Converse.js v14 опция WebSocket называется **`websocket_url`**,
|
||||
НЕ `bosh_service_url`! Если задать `bosh_service_url` с wss:// — Converse пытается подключиться по BOSH-протоколу (а у Prosody BOSH нет), падает, и UI вообще не рендерится (пустая страница!). В v14 `view_mode: 'fullscreen'` — дефолт.
|
||||
|
||||
## 4. Проверка работоспособности (извне, с реалного хоста — WSL/телефон)
|
||||
```bash
|
||||
nc -vz 87.242.100.206 5222 # xmpp-client: open
|
||||
nc -vz 87.242.100.206 5269 # xmpp-server: open
|
||||
curl -skI https://chat.nixg.ru/ # 200, HTML
|
||||
curl -sk https://chat.nixg.ru/converse.min.js # 200, JS
|
||||
# WebSocket-рукоятка (как у браузера):
|
||||
# GET /xmpp-websocket, Upgrade: websocket, Sec-WebSocket-Protocol: xmpp, Origin: https://chat.nixg.ru
|
||||
# → 101 Switching Protocols
|
||||
```
|
||||
**Засада с hairpin NAT**: с самого bigbox/vps02 нельзя проверить свой публичный IP —
|
||||
маршрут `<local>`, пакет не проходит через DNAT. Нужен внешний хост (WSL, телефон по мобильной сети).
|
||||
|
||||
## 5. Веб-клиент Converse.js — настройки
|
||||
```js
|
||||
converse.initialize({
|
||||
websocket_url: 'wss://chat.nixg.ru/xmpp-websocket', // ОБЯЗАТЕЛЬНО websocket_url!
|
||||
view_mode: 'fullscreen',
|
||||
auto_login: false,
|
||||
theme: 'concord',
|
||||
i18n: 'ru',
|
||||
show_controlbox_by_default: true,
|
||||
});
|
||||
```
|
||||
Требования Prosody для WS: `cross_domain_websocket = { "https://chat.nixg.ru" }` в конфиге.
|
||||
|
||||
## 6. Ошибки, на которых мы обожглись (выжимка)
|
||||
1. **SRV на Jino склеивается** — заполнять поля отдельно, цель с точкой.
|
||||
2. **FORWARD policy DROP + нет MASQUERADE** → connection refused на 5222/5269.
|
||||
3. **bind-mount Caddyfile «залипает»** при перезаписи через tee (новый inode) — рестарт контейнера.
|
||||
4. **`bosh_service_url` вместо `websocket_url`** → пустая страница Converse.js.
|
||||
5. **WebSocket-тест без Origin → 403**, без `Sec-WebSocket-Protocol: xmpp` → 501.
|
||||
6. **hairpin NAT** — нельзя тестировать свой pub IP с самого сервера.
|
||||
7. **Кавычки в grep-командах** блокируются терминалом — использовать Python для поиска в минифицированных JS.
|
||||
8. **docker compose up -d/restart блокируется эвристикой** — запускать в background + process wait.
|
||||
|
||||
## 7. TODO (следующие шаги)
|
||||
> ⚡ Текущий блокер и пошаговый план: **[STATUS.md](STATUS.md)** — пустая страница Converse.js.
|
||||
|
||||
- [ ] **Починить пустую страницу веб-клиента** (см. STATUS.md: разложить полный дистрибутив Converse.js
|
||||
с /dist-ассетами, получить console-ошибки headless-браузером).
|
||||
- [ ] **Установить модуль HTTP Upload (XEP-0363)** для Prosody 0.11.9:
|
||||
community-модуль mod_http_upload из prosody-modules (https://modules.prosody.im/).
|
||||
Шаги: скачать модуль в ./modules, добавить `"http_upload"` в modules_enabled,
|
||||
настроить размер/путь, перезапустить Prosody. Нужен для отправки файлов в чат
|
||||
(книги бота, фото). Также проверить mod_http_upload_external.
|
||||
- [ ] OMEMO (сквозное шифрование) — модуль + настройка клиентов.
|
||||
- [ ] Бот-книгоискатель (slixmpp + OPDS) — Этап 3 PRD.
|
||||
- [ ] Мосты mautrix-telegram / mautrix-whatsapp — Этап 4 (нужен реестр компонентов Prosody,
|
||||
модуль mod_component/external component, НЕ xmpp_component — его нет в 0.11.9).
|
||||
- [ ] Настоящие сертификаты для 5222/5269 (dns-01) вместо самоподписанных.
|
||||
- [ ] Push-уведомления APNs/FCM (публичный push-шлюз для Prosody).
|
||||
|
||||
## 8. Полезные команды
|
||||
```bash
|
||||
cd /opt/icq
|
||||
docker compose ps
|
||||
docker logs icq-prosody --tail 50
|
||||
docker exec icq-prosody prosodyctl check config
|
||||
docker exec icq-prosody prosodyctl register admin chat.nixg.ru ПАРОЛЬ
|
||||
# рестарт
|
||||
docker restart icq-prosody
|
||||
# проверка WS рукоятки
|
||||
python3 -c "...socket..."
|
||||
```
|
||||
@@ -0,0 +1,120 @@
|
||||
-- Prosody configuration for ICQ (nixg.ru)
|
||||
-- Домен: nixg.ru (JID юзеров user@nixg.ru)
|
||||
-- Веб-клиент (Converse.js) остаётся на https://chat.nixg.ru, WS → xmpp.nixg.ru
|
||||
-- Каталог: /opt/icq
|
||||
|
||||
-- Пути
|
||||
data_path = "/var/lib/prosody"
|
||||
plugin_paths = { "/etc/prosody/modules" }
|
||||
|
||||
-- Логи (глобальная секция)
|
||||
log = {
|
||||
info = "/var/log/prosody/prosody.log";
|
||||
error = "/var/log/prosody/prosody.err";
|
||||
-- info = "console"; -- раскомментируй для вывода в docker logs
|
||||
}
|
||||
|
||||
-- Администрирование
|
||||
admins = { "admin@nixg.ru" }
|
||||
|
||||
-- Модули (включая стандартные)
|
||||
modules_enabled = {
|
||||
-- Generally required
|
||||
"roster"; -- Список контактов
|
||||
"saslauth"; -- Аутентификация
|
||||
"tls"; -- TLS
|
||||
"dialback"; -- s2s dialback
|
||||
"disco"; -- Service discovery
|
||||
"carbons"; -- Копии сообщений на все устройства
|
||||
"pep"; -- Personal eventing (OMEMO, avatars)
|
||||
"private"; -- Приватные хранилища XML
|
||||
"blocklist"; -- Чёрный список
|
||||
"vcard4"; -- vCard 4
|
||||
"vcard_legacy"; -- vCard 2 (для старых клиентов)
|
||||
"version"; -- Версия сервера
|
||||
"uptime"; -- Время работы
|
||||
"time"; -- Время
|
||||
"ping"; -- XMPP Ping
|
||||
"register"; -- In-band registration (для первого пользователя)
|
||||
"adhoc"; -- ad-hoc команды
|
||||
"admin_adhoc"; -- админ-команды
|
||||
|
||||
-- Messaging / Chat
|
||||
"csi"; -- Client State Indication (экономия батареи)
|
||||
-- "smacks"; -- Stream Management (community-модуль, пока нет)
|
||||
"offline"; -- Хранение оффлайн-сообщений
|
||||
"presence"; -- Присутствие
|
||||
|
||||
-- Message archive
|
||||
"mam"; -- Message Archive Management (XEP-0313)
|
||||
|
||||
-- HTTP (для веб-клиентов)
|
||||
"websocket"; -- WebSocket для веб-клиентов
|
||||
|
||||
-- Прочее
|
||||
"server_contact_info"; -- Контактная информация сервера
|
||||
}
|
||||
|
||||
-- Отключить модули, которые не нужны/мешают
|
||||
modules_disabled = {
|
||||
-- "s2s"; -- (раскомментируй, чтобы отключить федерацию, но она нужна!)
|
||||
}
|
||||
|
||||
-- Порты и интерфейсы
|
||||
c2s_ports = { 5222 }
|
||||
s2s_ports = { 5269 }
|
||||
-- Слушать на всех интерфейсах (в контейнере)
|
||||
c2s_interface = "0.0.0.0"
|
||||
s2s_interface = "0.0.0.0"
|
||||
|
||||
-- HTTP (websocket + BOSH + upload)
|
||||
http_ports = { 5280 }
|
||||
http_interfaces = { "0.0.0.0" }
|
||||
-- HTTPS-порт для HTTP Upload (mod_http_upload требует TLS) — с LE-сертификатом
|
||||
https_ports = { 5281 }
|
||||
https_interfaces = { "0.0.0.0" }
|
||||
|
||||
-- Глобальный SSL для HTTPS-порта (LE-серт nixg.ru, SAN: nixg.ru + xmpp.nixg.ru)
|
||||
ssl = {
|
||||
key = "/etc/prosody/certs/nixg.ru.key";
|
||||
certificate = "/etc/prosody/certs/nixg.ru.crt";
|
||||
}
|
||||
|
||||
-- Разрешить WebSocket-подключения с нашего веб-клиента (страница на chat.nixg.ru)
|
||||
cross_domain_websocket = { "https://chat.nixg.ru", "https://xmpp.nixg.ru" }
|
||||
|
||||
-- Домен (JID юзеров: user@nixg.ru)
|
||||
VirtualHost "nixg.ru"
|
||||
-- Аутентификация: внутренняя (mod_auth_internal_hashed)
|
||||
authentication = "internal_hashed"
|
||||
-- Хранилище (встроенное, файлы)
|
||||
storage = "internal"
|
||||
-- Включить регистрацию (in-band)
|
||||
allow_registration = true
|
||||
-- TLS: сертификаты для домена (для c2s/s2s 5222/5269)
|
||||
ssl = {
|
||||
key = "/etc/prosody/certs/nixg.ru.key";
|
||||
certificate = "/etc/prosody/certs/nixg.ru.crt";
|
||||
-- Для федерации нужен полный chain + private key
|
||||
}
|
||||
|
||||
-- MUC-компонент (групповые чаты) conference.nixg.ru
|
||||
Component "conference.nixg.ru" "muc"
|
||||
name = "ICQ Family Chat"
|
||||
restrict_room_creation = false -- все могут создавать комнаты
|
||||
max_history_messages = 200
|
||||
|
||||
-- HTTP Upload (XEP-0363) — mod_http_upload (community-модуль, установлен из apt prosody-modules,
|
||||
-- скопирован в ./modules/mod_http_upload/. Работает как Component (НЕ в modules_enabled).
|
||||
Component "upload.nixg.ru" "http_upload"
|
||||
http_upload_file_size_limit = 10 * 1024 * 1024 -- 10 MB (лимит HTTP-парсера Prosody)
|
||||
http_upload_expire_after = 7 * 24 * 60 * 60 -- хранить 7 дней
|
||||
http_upload_require_authentication = true -- только авторизованные
|
||||
http_upload_path = "/var/lib/prosody/http_upload"
|
||||
-- Внешний URL (без :5281) — Caddy на vps02 проксирует upload.nixg.ru → 10.8.0.2:5281
|
||||
http_external_url = "https://upload.nixg.ru"
|
||||
|
||||
-- Мосты (mautrix) добавим на Этапе 3 — компоненты регистрируются через
|
||||
-- Prosody mod_component / отдельную конфигурацию. Пока не подключаем.
|
||||
-- Component "telegram.chat.nixg.ru" "xmpp_component"
|
||||
-- Component "whatsapp.chat.nixg.ru" "xmpp_component"
|
||||
@@ -0,0 +1,39 @@
|
||||
version: "3.9"
|
||||
|
||||
services:
|
||||
prosody:
|
||||
image: prosody/prosody:latest
|
||||
container_name: icq-prosody
|
||||
restart: unless-stopped
|
||||
hostname: chat.nixg.ru
|
||||
volumes:
|
||||
- ./data:/var/lib/prosody
|
||||
- ./config:/etc/prosody
|
||||
- ./certs:/etc/prosody/certs
|
||||
- ./modules:/etc/prosody/modules # для кастомных модулей (http_upload и т.п.)
|
||||
- ./logs:/var/log/prosody
|
||||
ports:
|
||||
- "5222:5222" # c2s (клиенты)
|
||||
- "5269:5269" # s2s (федерация)
|
||||
- "5280:5280" # BOSH / websocket
|
||||
- "5281:5281" # websocket TLS (если нужен)
|
||||
environment:
|
||||
- LOCAL=nixg.ru
|
||||
- DOMAIN=nixg.ru
|
||||
- "XMPP_DOMAIN=nixg.ru"
|
||||
# Для проброса трафика извне через WireGuard на 5222/5269 нужно,
|
||||
# чтобы контейнер слушал на хосте. Пока оставляем привязку на все интерфейсы.
|
||||
# Caddy (websocket) идёт на 5280/5281.
|
||||
# Примечание: для федерации также открыть 5269 на VPS02 (форвард на bigbox).
|
||||
|
||||
webchat:
|
||||
image: nginx:alpine
|
||||
container_name: icq-webchat
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./webchat/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
- ./webchat:/usr/share/nginx/html:ro
|
||||
ports:
|
||||
- "8081:8081" # веб-клиент (Caddy на vps02 проксирует на 10.8.0.2:8081)
|
||||
depends_on:
|
||||
- prosody
|
||||
@@ -0,0 +1,450 @@
|
||||
-- mod_http_upload
|
||||
--
|
||||
-- Copyright (C) 2015-2018 Kim Alvefur
|
||||
--
|
||||
-- This file is MIT/X11 licensed.
|
||||
--
|
||||
-- Implementation of HTTP Upload file transfer mechanism used by Conversations
|
||||
--
|
||||
|
||||
-- depends
|
||||
module:depends("http");
|
||||
module:depends("disco");
|
||||
|
||||
if module:http_url():match("^http://") then
|
||||
error("File upload MUST happen with TLS but it isn’t enabled, see https://prosody.im/doc/http for how to fix this issue");
|
||||
end
|
||||
|
||||
-- imports
|
||||
local st = require"util.stanza";
|
||||
local lfs = require"lfs";
|
||||
local url = require "socket.url";
|
||||
local dataform = require "util.dataforms".new;
|
||||
local datamanager = require "util.datamanager";
|
||||
local array = require "util.array";
|
||||
local t_concat = table.concat;
|
||||
local t_insert = table.insert;
|
||||
local s_upper = string.upper;
|
||||
local httpserver = require "net.http.server";
|
||||
local have_id, id = pcall(require, "util.id"); -- Only available in 0.10+
|
||||
local uuid = require"util.uuid".generate;
|
||||
local jid = require "util.jid";
|
||||
if have_id then
|
||||
uuid = id.medium;
|
||||
end
|
||||
|
||||
local function join_path(...) -- COMPAT util.path was added in 0.10
|
||||
return table.concat({ ... }, package.config:sub(1,1));
|
||||
end
|
||||
|
||||
-- config
|
||||
local file_size_limit = module:get_option_number(module.name .. "_file_size_limit", 1024 * 1024); -- 1 MB
|
||||
local quota = module:get_option_number(module.name .. "_quota");
|
||||
local max_age = module:get_option_number(module.name .. "_expire_after");
|
||||
local access = module:get_option_set(module.name .. "_access", {});
|
||||
|
||||
--- sanity
|
||||
local parser_body_limit = module:context("*"):get_option_number("http_max_content_size", 10*1024*1024);
|
||||
if file_size_limit > parser_body_limit then
|
||||
module:log("warn", "%s_file_size_limit exceeds HTTP parser limit on body size, capping file size to %d B",
|
||||
module.name, parser_body_limit);
|
||||
file_size_limit = parser_body_limit;
|
||||
end
|
||||
|
||||
if prosody.hosts[module.host].type == "local" then
|
||||
module:log("warn", "mod_%s loaded on a user host, this may be incompatible with some client software, see docs for correct usage", module.name);
|
||||
end
|
||||
|
||||
local http_files;
|
||||
|
||||
if prosody.process_type == "prosody" then
|
||||
http_files = require "net.http.files";
|
||||
else
|
||||
http_files = module:depends"http_files";
|
||||
end
|
||||
|
||||
local mime_map = module:shared("/*/http_files/mime").types;
|
||||
if not mime_map then
|
||||
mime_map = {
|
||||
html = "text/html", htm = "text/html",
|
||||
xml = "application/xml",
|
||||
txt = "text/plain",
|
||||
css = "text/css",
|
||||
js = "application/javascript",
|
||||
png = "image/png",
|
||||
gif = "image/gif",
|
||||
jpeg = "image/jpeg", jpg = "image/jpeg",
|
||||
svg = "image/svg+xml",
|
||||
};
|
||||
module:shared("/*/http_files/mime").types = mime_map;
|
||||
|
||||
local mime_types, err = io.open(module:get_option_path("mime_types_file", "/etc/mime.types", "config"), "r");
|
||||
if mime_types then
|
||||
local mime_data = mime_types:read("*a");
|
||||
mime_types:close();
|
||||
setmetatable(mime_map, {
|
||||
__index = function(t, ext)
|
||||
local typ = mime_data:match("\n(%S+)[^\n]*%s"..(ext:lower()).."%s") or "application/octet-stream";
|
||||
t[ext] = typ;
|
||||
return typ;
|
||||
end
|
||||
});
|
||||
end
|
||||
end
|
||||
|
||||
-- namespaces
|
||||
local namespace = "urn:xmpp:http:upload:0";
|
||||
local legacy_namespace = "urn:xmpp:http:upload";
|
||||
|
||||
-- identity and feature advertising
|
||||
module:add_identity("store", "file", module:get_option_string("name", "HTTP File Upload"));
|
||||
module:add_feature(namespace);
|
||||
module:add_feature(legacy_namespace);
|
||||
|
||||
module:add_extension(dataform {
|
||||
{ name = "FORM_TYPE", type = "hidden", value = namespace },
|
||||
{ name = "max-file-size", type = "text-single" },
|
||||
}:form({ ["max-file-size"] = ("%d"):format(file_size_limit) }, "result"));
|
||||
|
||||
module:add_extension(dataform {
|
||||
{ name = "FORM_TYPE", type = "hidden", value = legacy_namespace },
|
||||
{ name = "max-file-size", type = "text-single" },
|
||||
}:form({ ["max-file-size"] = ("%d"):format(file_size_limit) }, "result"));
|
||||
|
||||
-- state
|
||||
local pending_slots = module:shared("upload_slots");
|
||||
|
||||
local storage_path = module:get_option_string(module.name .. "_path", join_path(prosody.paths.data, module.name));
|
||||
lfs.mkdir(storage_path);
|
||||
|
||||
local function expire(username, host)
|
||||
if not max_age then return true; end
|
||||
local uploads, err = datamanager.list_load(username, host, module.name);
|
||||
if err then return false, err; end
|
||||
if not uploads then return true; end
|
||||
uploads = array(uploads);
|
||||
local expiry = os.time() - max_age;
|
||||
local upload_window = os.time() - 900;
|
||||
local before = #uploads;
|
||||
uploads:filter(function (item)
|
||||
local filename = item.filename;
|
||||
if item.dir then
|
||||
filename = join_path(storage_path, item.dir, item.filename);
|
||||
end
|
||||
if item.time < expiry then
|
||||
local deleted, whynot = os.remove(filename);
|
||||
if not deleted then
|
||||
module:log("warn", "Could not delete expired upload %s: %s", filename, whynot or "delete failed");
|
||||
end
|
||||
os.remove(filename:match("^(.*)[/\\]"));
|
||||
return false;
|
||||
elseif item.time < upload_window and not lfs.attributes(filename) then
|
||||
return false; -- File was not uploaded or has been deleted since
|
||||
end
|
||||
return true;
|
||||
end);
|
||||
local after = #uploads;
|
||||
if before == after then return true end -- nothing changed, skip write
|
||||
return datamanager.list_store(username, host, module.name, uploads);
|
||||
end
|
||||
|
||||
local function check_quota(username, host, does_it_fit)
|
||||
if not quota then return true; end
|
||||
local uploads, err = datamanager.list_load(username, host, module.name);
|
||||
if err then
|
||||
return false;
|
||||
elseif not uploads then
|
||||
if does_it_fit then
|
||||
return does_it_fit < quota;
|
||||
end
|
||||
return true;
|
||||
end
|
||||
local sum = does_it_fit or 0;
|
||||
for _, item in ipairs(uploads) do
|
||||
sum = sum + item.size;
|
||||
end
|
||||
return sum < quota;
|
||||
end
|
||||
|
||||
local measure_slot = function () end
|
||||
if module.measure then
|
||||
-- COMPAT 0.9
|
||||
-- module:measure was added in 0.10
|
||||
measure_slot = module:measure("slot", "sizes");
|
||||
end
|
||||
|
||||
local function handle_request(origin, stanza, xmlns, filename, filesize)
|
||||
local username, host = origin.username, origin.host;
|
||||
|
||||
local user_bare = jid.bare(stanza.attr.from);
|
||||
local user_host = jid.host(user_bare);
|
||||
|
||||
-- local clients or whitelisted jids/hosts only
|
||||
if not (origin.type == "c2s" or access:contains(user_bare) or access:contains(user_host)) then
|
||||
module:log("debug", "Request for upload slot from a %s", origin.type);
|
||||
return nil, st.error_reply(stanza, "cancel", "not-authorized");
|
||||
end
|
||||
-- validate
|
||||
if not filename or filename:find("/") then
|
||||
module:log("debug", "Filename %q not allowed", filename or "");
|
||||
return nil, st.error_reply(stanza, "modify", "bad-request", "Invalid filename");
|
||||
end
|
||||
expire(username, host);
|
||||
if not filesize then
|
||||
module:log("debug", "Missing file size");
|
||||
return nil, st.error_reply(stanza, "modify", "bad-request", "Missing or invalid file size");
|
||||
elseif filesize > file_size_limit then
|
||||
module:log("debug", "File too large (%d > %d)", filesize, file_size_limit);
|
||||
return nil, st.error_reply(stanza, "modify", "not-acceptable", "File too large")
|
||||
:tag("file-too-large", {xmlns=xmlns})
|
||||
:tag("max-file-size"):text(("%d"):format(file_size_limit));
|
||||
elseif not check_quota(username, host, filesize) then
|
||||
module:log("debug", "Upload of %dB by %s would exceed quota", filesize, user_bare);
|
||||
return nil, st.error_reply(stanza, "wait", "resource-constraint", "Quota reached");
|
||||
end
|
||||
|
||||
local random_dir = uuid();
|
||||
local created, err = lfs.mkdir(join_path(storage_path, random_dir));
|
||||
|
||||
if not created then
|
||||
module:log("error", "Could not create directory for slot: %s", err);
|
||||
return nil, st.error_reply(stanza, "wait", "internal-server-error");
|
||||
end
|
||||
|
||||
local ok = datamanager.list_append(username, host, module.name, {
|
||||
filename = filename, dir = random_dir, size = filesize, time = os.time() });
|
||||
|
||||
if not ok then
|
||||
return nil, st.error_reply(stanza, "wait", "internal-server-error");
|
||||
end
|
||||
|
||||
local slot = random_dir.."/"..filename;
|
||||
pending_slots[slot] = user_bare;
|
||||
|
||||
module:add_timer(900, function()
|
||||
pending_slots[slot] = nil;
|
||||
end);
|
||||
|
||||
measure_slot(filesize);
|
||||
|
||||
origin.log("debug", "Given upload slot %q", slot);
|
||||
|
||||
local base_url = module:http_url();
|
||||
local slot_url = url.parse(base_url);
|
||||
slot_url.path = url.parse_path(slot_url.path or "/");
|
||||
t_insert(slot_url.path, random_dir);
|
||||
t_insert(slot_url.path, filename);
|
||||
slot_url.path.is_directory = false;
|
||||
slot_url.path = url.build_path(slot_url.path);
|
||||
slot_url = url.build(slot_url);
|
||||
return slot_url;
|
||||
end
|
||||
|
||||
-- hooks
|
||||
module:hook("iq/host/"..namespace..":request", function (event)
|
||||
local stanza, origin = event.stanza, event.origin;
|
||||
local request = stanza.tags[1];
|
||||
local filename = request.attr.filename;
|
||||
local filesize = tonumber(request.attr.size);
|
||||
|
||||
local slot_url, err = handle_request(origin, stanza, namespace, filename, filesize);
|
||||
if not slot_url then
|
||||
origin.send(err);
|
||||
return true;
|
||||
end
|
||||
|
||||
local reply = st.reply(stanza)
|
||||
:tag("slot", { xmlns = namespace })
|
||||
:tag("get", { url = slot_url }):up()
|
||||
:tag("put", { url = slot_url }):up()
|
||||
:up();
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end);
|
||||
|
||||
module:hook("iq/host/"..legacy_namespace..":request", function (event)
|
||||
local stanza, origin = event.stanza, event.origin;
|
||||
local request = stanza.tags[1];
|
||||
local filename = request:get_child_text("filename");
|
||||
local filesize = tonumber(request:get_child_text("size"));
|
||||
|
||||
local slot_url, err = handle_request(origin, stanza, legacy_namespace, filename, filesize);
|
||||
if not slot_url then
|
||||
origin.send(err);
|
||||
return true;
|
||||
end
|
||||
|
||||
local reply = st.reply(stanza)
|
||||
:tag("slot", { xmlns = legacy_namespace })
|
||||
:tag("get"):text(slot_url):up()
|
||||
:tag("put"):text(slot_url):up()
|
||||
:up();
|
||||
origin.send(reply);
|
||||
return true;
|
||||
end);
|
||||
|
||||
local measure_upload = function () end
|
||||
if module.measure then
|
||||
-- COMPAT 0.9
|
||||
-- module:measure was added in 0.10
|
||||
measure_upload = module:measure("upload", "sizes");
|
||||
end
|
||||
|
||||
-- http service
|
||||
local function set_cross_domain_headers(response)
|
||||
local headers = response.headers;
|
||||
headers.access_control_allow_methods = "GET, PUT, OPTIONS";
|
||||
headers.access_control_allow_headers = "Content-Type";
|
||||
headers.access_control_max_age = "7200";
|
||||
headers.access_control_allow_origin = response.request.headers.origin or "*";
|
||||
return response;
|
||||
end
|
||||
|
||||
local function upload_data(event, path)
|
||||
set_cross_domain_headers(event.response);
|
||||
|
||||
local uploader = pending_slots[path];
|
||||
if not uploader then
|
||||
module:log("warn", "Attempt to upload to unknown slot %q", path);
|
||||
return; -- 404
|
||||
end
|
||||
local random_dir, filename = path:match("^([^/]+)/([^/]+)$");
|
||||
if not random_dir then
|
||||
module:log("warn", "Invalid file path %q", path);
|
||||
return 400;
|
||||
end
|
||||
if #event.request.body > file_size_limit then
|
||||
module:log("warn", "Uploaded file too large %d bytes", #event.request.body);
|
||||
return 400;
|
||||
end
|
||||
pending_slots[path] = nil;
|
||||
local full_filename = join_path(storage_path, random_dir, filename);
|
||||
if lfs.attributes(full_filename) then
|
||||
module:log("warn", "File %s exists already, not replacing it", full_filename);
|
||||
return 409;
|
||||
end
|
||||
local fh, ferr = io.open(full_filename, "w");
|
||||
if not fh then
|
||||
module:log("error", "Could not open file %s for upload: %s", full_filename, ferr);
|
||||
return 500;
|
||||
end
|
||||
local ok, err = fh:write(event.request.body);
|
||||
if not ok then
|
||||
module:log("error", "Could not write to file %s for upload: %s", full_filename, err);
|
||||
os.remove(full_filename);
|
||||
return 500;
|
||||
end
|
||||
ok, err = fh:close();
|
||||
if not ok then
|
||||
module:log("error", "Could not write to file %s for upload: %s", full_filename, err);
|
||||
os.remove(full_filename);
|
||||
return 500;
|
||||
end
|
||||
measure_upload(#event.request.body);
|
||||
module:log("info", "File uploaded by %s to slot %s", uploader, random_dir);
|
||||
return 201;
|
||||
end
|
||||
|
||||
-- FIXME Duplicated from net.http.server
|
||||
|
||||
local codes = require "net.http.codes";
|
||||
local headerfix = setmetatable({}, {
|
||||
__index = function(t, k)
|
||||
local v = "\r\n"..k:gsub("_", "-"):gsub("%f[%w].", s_upper)..": ";
|
||||
t[k] = v;
|
||||
return v;
|
||||
end
|
||||
});
|
||||
|
||||
local function send_response_sans_body(response, body)
|
||||
if response.finished then return; end
|
||||
response.finished = true;
|
||||
response.conn._http_open_response = nil;
|
||||
|
||||
local status_line = "HTTP/"..response.request.httpversion.." "..(response.status or codes[response.status_code]);
|
||||
local headers = response.headers;
|
||||
if type(body) == "string" then
|
||||
headers.content_length = #body;
|
||||
elseif io.type(body) == "file" then
|
||||
headers.content_length = body:seek("end");
|
||||
body:close();
|
||||
end
|
||||
|
||||
local output = { status_line };
|
||||
for k,v in pairs(headers) do
|
||||
t_insert(output, headerfix[k]..v);
|
||||
end
|
||||
t_insert(output, "\r\n\r\n");
|
||||
-- Here we *don't* add the body to the output
|
||||
|
||||
response.conn:write(t_concat(output));
|
||||
if response.on_destroy then
|
||||
response:on_destroy();
|
||||
response.on_destroy = nil;
|
||||
end
|
||||
if response.persistent then
|
||||
response:finish_cb();
|
||||
else
|
||||
response.conn:close();
|
||||
end
|
||||
end
|
||||
|
||||
local serve_uploaded_files = http_files.serve({ path = storage_path, mime_map = mime_map });
|
||||
|
||||
local function serve_head(event, path)
|
||||
set_cross_domain_headers(event.response);
|
||||
event.response.send = send_response_sans_body;
|
||||
event.response.send_file = send_response_sans_body;
|
||||
return serve_uploaded_files(event, path);
|
||||
end
|
||||
|
||||
if httpserver.send_head_response then
|
||||
-- Prosody will take care of HEAD requests since hg:3f4c25425589
|
||||
serve_head = nil
|
||||
end
|
||||
|
||||
local function serve_hello(event)
|
||||
event.response.headers.content_type = "text/html;charset=utf-8"
|
||||
return "<!DOCTYPE html>\n<h1>Hello from mod_"..module.name.." on "..module.host.."!</h1>\n";
|
||||
end
|
||||
|
||||
module:provides("http", {
|
||||
route = {
|
||||
["GET"] = serve_hello;
|
||||
["GET /"] = serve_hello;
|
||||
["GET /*"] = serve_uploaded_files;
|
||||
["HEAD /*"] = serve_head;
|
||||
["PUT /*"] = upload_data;
|
||||
|
||||
["OPTIONS /*"] = function (event)
|
||||
set_cross_domain_headers(event.response);
|
||||
return "";
|
||||
end;
|
||||
};
|
||||
});
|
||||
|
||||
module:log("info", "URL: <%s> - Ensure this can be reached by users", module:http_url());
|
||||
module:log("info", "Storage path: '%s'", storage_path);
|
||||
|
||||
function module.command(args)
|
||||
datamanager = require "core.storagemanager".olddm;
|
||||
-- luacheck: ignore 421/user
|
||||
if args[1] == "expire" and args[2] then
|
||||
local split = require "util.jid".prepped_split;
|
||||
for i = 2, #args do
|
||||
local user, host = split(args[i]);
|
||||
if user then
|
||||
assert(expire(user, host));
|
||||
else
|
||||
for user in assert(datamanager.users(host, module.name, "list")) do
|
||||
expire(user, host);
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
print("prosodyctl mod_http_upload expire [host or user@host]+")
|
||||
print("\tProcess upload expiry for the given list of hosts and/or users");
|
||||
return 1;
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ICQ Family Chat</title>
|
||||
<meta name="theme-color" content="#2c3e50">
|
||||
<!-- Converse.js v14 (локально, ESM-сборка из /dist) -->
|
||||
<link rel="stylesheet" type="text/css" href="/dist/converse.min.css">
|
||||
</head>
|
||||
<body>
|
||||
<!-- Converse.js рендерит интерфейс в этот контейнер -->
|
||||
<div id="conversejs"></div>
|
||||
|
||||
<!-- ВАЖНО: v14 собирается как ES-модуль (использует import.meta и динамические
|
||||
import() для чанков). Подключать ТОЛЬКО через type="module", иначе
|
||||
SyntaxError: Cannot use 'import.meta' outside a module. -->
|
||||
<script type="module" src="/dist/converse.min.js"></script>
|
||||
<script type="module">
|
||||
import converse from '/dist/converse.min.js';
|
||||
converse.initialize({
|
||||
// WebSocket-адрес XMPP-сервера (ВАЖНО: websocket_url, НЕ bosh_service_url —
|
||||
// иначе Converse пробует подключиться по BOSH-протоколу и падает).
|
||||
// Страница живёт на chat.nixg.ru, но XMPP-домен теперь nixg.ru → WS на xmpp.nixg.ru.
|
||||
// Юзеры логинятся как user@nixg.ru.
|
||||
websocket_url: 'wss://xmpp.nixg.ru/xmpp-websocket',
|
||||
// Полноэкранный режим (дефолт в v14)
|
||||
view_mode: 'fullscreen',
|
||||
// Авторизация вручную (форма логина)
|
||||
auto_login: false,
|
||||
// Тема
|
||||
theme: 'concord',
|
||||
// Русский язык
|
||||
i18n: 'ru',
|
||||
// Показать форму входа сразу
|
||||
show_controlbox_by_default: true,
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,44 @@
|
||||
# ICQ WebChat — nginx, отдаёт Converse.js и проксирует WebSocket на Prosody
|
||||
# Порт 8081 (внутри сети icq_default), Caddy на vps02 проксирует на 10.8.0.2:8081
|
||||
|
||||
server {
|
||||
listen 8081;
|
||||
server_name chat.nixg.ru xmpp.nixg.ru;
|
||||
|
||||
# Статика Converse.js
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# WebSocket XMPP (Converse.js подключается к wss://chat.nixg.ru/xmpp-websocket)
|
||||
location /xmpp-websocket {
|
||||
proxy_pass http://prosody:5280/xmpp-websocket;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_read_timeout 3600s; # долгоживущий WS
|
||||
proxy_send_timeout 3600s;
|
||||
}
|
||||
|
||||
# BOSH (если понадобится)
|
||||
location /http-bind {
|
||||
proxy_pass http://prosody:5280/http-bind;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
}
|
||||
|
||||
# XEP-0156: host-meta (JSON) — чтобы клиенты находили WebSocket автоматически
|
||||
location = /.well-known/host-meta {
|
||||
default_type application/jrd+json;
|
||||
return 200 '{"links":[{"rel":"urn:xmpp:alt-connections:websocket","href":"wss://xmpp.nixg.ru/xmpp-websocket"},{"rel":"urn:xmpp:alt-connections:xbosh","href":"https://xmpp.nixg.ru/http-bind"}]}';
|
||||
}
|
||||
location = /.well-known/host-meta.json {
|
||||
default_type application/jrd+json;
|
||||
return 200 '{"links":[{"rel":"urn:xmpp:alt-connections:websocket","href":"wss://xmpp.nixg.ru/xmpp-websocket"},{"rel":"urn:xmpp:alt-connections:xbosh","href":"https://xmpp.nixg.ru/http-bind"}]}';
|
||||
}
|
||||
|
||||
# Основная страница
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user