MessageCenter — это всегда тот же сервис, в коде и переменных он называется MSGW.
Лёгкий сервис для обмена сообщениями и безопасной проксировки запросов. Запускается одним контейнером, настраивается переменными окружения.
Представьте, что у вас есть несколько сервисов, которым нужно обмениваться данными. MSGW решает две задачи:
- Шина сообщений — как чат с историей: отправитель кладёт сообщение, все подключённые получатели получают его мгновенно. Если получатель был отключён — сообщение сохранится в хранилище и дойдёт, когда он подключится.
- Безопасный прокси — как почтовый ящик, который вскрывает зашифрованный конверт и доставляет содержимое адресату. Вы отправляете POST-запрос через MSGW, он расшифровывает тело и пересылает дальше на реальный бэкенд.
Эти режимы работают независимо — можно использовать только один из них.
За 3 команды:
# 1. Сеть для контейнеров
docker network create msgw-net
# 2. Redis для хранения сообщений
docker run -d --name redis --network msgw-net redis --appendonly yes
# 3. MSGW (без шифрования)
docker run -d --name msgw --network msgw-net -p 8000:8000 \
-e MSGW_CACHE_URL=redis://redis:6379/0 \
ghcr.io/grevinden/msgw:latestГотово. Сервис слушает на http://localhost:8000.
Чтобы включить режим прокси с расшифровкой, добавьте ключ:
docker run -d --name msgw --network msgw-net -p 8000:8000 \
-e MSGW_CACHE_URL=redis://redis:6379/0 \
-e MSGW_ECIES_KEY="ваш_приватный_ключ_base64url" \
ghcr.io/grevinden/msgw:latest| Режим | Адрес | Описание |
|---|---|---|
| WebSocket | ws://host:8000/<путь> |
Подключайтесь и получайте сообщения в реальном времени |
| HTTP QUERY | http://host:8000/<путь> |
Отправляйте сообщения через HTTP (метод QUERY) |
| Прокси | POST http://host:8000/<путь>?upstream=... |
Пересылка запросов на бэкенд с расшифровкой (нужен ключ) |
Отправитель ──┐
├──→ MSGW → Redis →→ рассылка всем подключённым клиентам
Другой клиент ─┘
- Любой клиент подключается по WebSocket к любому пути. Путь не создаёт изолированный канал — все подключённые клиенты (включая отправителя) получают все сообщения.
- Сообщение отправляется через WebSocket или HTTP QUERY.
- MSGW сохраняет его в хранилище (Redis, MongoDB или память) и рассылает всем подключённым WebSocket-клиентам.
- Если клиент отключился, сообщение не потеряется — он получит его при reconnect.
{
"uuid": "уникальный-идентификатор",
"ttl": 3600,
"payload": { "typ": "send", "top": "https://example.com", "mes": "текст" }
}| Поле | Описание |
|---|---|
uuid |
Уникальный ID сообщения (UUID4). Запись с тем же uuid заменяет предыдущее значение в кэше |
ttl |
Время жизни в секундах. Должно быть строго > 0. Если отсутствует — берётся из MSGW_CACHE_TTL |
payload |
Дискриминированный union. Тип определяется полем typ |
payload принимает одну из трёх форм, определяемых полем typ:
typ |
Описание | Обязательные поля |
|---|---|---|
send |
Новое сообщение. Рассылается всем | top (URL) — обязательный валидный URL, mes (строка, ≥1 символ) — текст сообщения |
done |
Сообщение обработано. Заменяет предыдущее значение с тем же uuid в кэше |
(нет дополнительных полей) |
fail |
Ошибка обработки | err — описание ошибки (str или list[str]) |
Сервер добавляет вычисляемые поля в каждый ответ:
| Поле | Описание |
|---|---|
ulid |
Служебный идентификатор на основе timestamp (каждый вызов — новый ULID) |
typ |
Классификация: notify (для send), receipt (для done/fail), unknown |
При подключении нового WebSocket-клиента сервер отправляет все сообщения из кэша (без фильтрации по пути WebSocket), а не только последнее по uuid.
Если входящий JSON не проходит валидацию, но содержит распознаваемый uuid, сервер автоматически создаёт fail-сообщение с этим uuid и текстом ошибки. Полностью невалидные данные (без uuid) отбрасываются. Это относится к WebSocket-сообщениям; HTTP QUERY возвращает стандартную ошибку валидации (422).
JavaScript
const ws = new WebSocket("ws://localhost:8000/chat/general");
ws.onopen = () => ws.send(JSON.stringify({
uuid: crypto.randomUUID(),
ttl: 3600,
payload: { typ: "send", top: "https://t.me/example", mes: "Привет" }
}));
ws.onmessage = e => console.log(JSON.parse(e.data));Python
import websockets, json, uuid
async with websockets.connect("ws://localhost:8000/chat/general") as ws:
await ws.send(json.dumps({
"uuid": str(uuid.uuid4()), "ttl": 3600,
"payload": {"typ": "send", "top": "https://example.com", "mes": "Привет"}
}))
async for msg in ws:
print(json.loads(msg))Если у вас нет WebSocket-клиента, используйте HTTP-метод QUERY (это не POST и не GET — собственный метод, который понимает MSGW):
curl -X QUERY http://localhost:8000/chat/general \
-H "Content-Type: application/json" \
-d '{
"uuid": "550e8400-e29b-41d4-a716-446655440000",
"ttl": 3600,
"payload": {"typ": "send", "top": "https://example.com", "mes": "Сервер перезагружен"}
}'Ответ 200 OK — сообщение принято, сохранено и разослано.
Включается только при заданной переменной
MSGW_ECIES_KEY. Без ключа этот режим не работает.
У вас есть бэкенд, который не должен быть доступен напрямую. MSGW стоит перед ним и:
- Принимает POST-запросы
- Расшифровывает зашифрованные фрагменты в теле запроса
- Пересылает чистый запрос на бэкенд
«Upstream» — это просто адрес реального бэкенда, куда MSGW пересылает запрос. Указывается через параметр ?upstream=....
Важно: URL в
upstreamне должен содержать query-параметров (например,?upstream=http://backend:8080/notify— допустимо, а?upstream=http://backend:8080/notify?foo=bar— отклонено). Дополнительные параметры передаются как отдельные query-параметры запроса к MSGW.
Клиент ──POST──→ MSGW ──health-чек──→ MSGW ──расшифровка──→ MSGW ──POST──→ Бэкенд (upstream)
- Клиент отправляет POST на любой путь MSGW, например
/notify. - В URL указывает
?upstream=http://backend:8080/notify— куда переслать. - В теле JSON зашифрованные секции оборачиваются в
{{...}}. Внутри — base64url-строка длиной ≥ 43 символа ([A-Za-z0-9_-]). - MSGW находит все
{{...}}, расшифровывает их своим приватным ключом и подставляет открытые значения. - Полученный чистый JSON отправляется на upstream.
Путь в запросе к MSGW (/notify) не передаётся бэкенду — бэкенд получает путь из upstream.
Исходный запрос через MSGW:
curl -X POST "http://localhost:8000/notify?upstream=http://backend:8080/notify&chat_id=777" \
-H "Content-Type: application/json" \
-d '{
"urls": [
"tgram://token:{{A3k9FmN2pQr7xYw8bVc1dHj4eLt6gUs0iOz5KfR9WmX2BnD}}/chat"
],
"body": "тест"
}'Что получит бэкенд на http://backend:8080/notify?chat_id=777:
{
"urls": ["tgram://token:открытый_секрет/chat"],
"body": "тест"
}MSGW использует ECIES (X25519 + ChaCha20-Poly1305). Простыми словами:
- У MSGW есть приватный ключ (хранится в
MSGW_ECIES_KEY). - Из него можно получить публичный ключ — его можно безопасно раздать клиентам.
- Клиент шифрует данные публичным ключом → только MSGW может их расшифровать.
- Зашифрованный фрагмент оборачивается в
{{...}}внутри JSON. MSGW ищет только фрагменты вида{{base64url_строка}}длиной ≥ 43 символа ([A-Za-z0-9_-]).
MSGW сканирует тело запроса, находит все {{...}}, расшифровывает и подставляет. Если расшифровка не удалась — фрагмент остаётся как есть, запрос всё равно уходит на бэкенд.
- Все параметры, кроме
upstream, передаются на бэкенд. upstreamудаляется из запроса перед пересылкой.
| Код | Причина |
|---|---|
502 |
Upstream недоступен: health-чек (TCP-проверка порта) или ошибка подключения. MSGW проверяет, что порт upstream открыт, но не отправляет HTTP-запросы. Первый запрос к новому хосту включает проверку (до 2 сек). Последующие запросы используют результат фонового чекера (каждые 3 сек) |
500 |
Внутренняя ошибка MSGW |
Приватный ключ — 32 случайных байта, закодированных в base64url без = (ровно 43 символа).
# Через WireGuard (wg)
wg genkey | basenc --base64url -w0 | tr -d '='
# Через openssl
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='Публичный ключ для клиентов (генерируется из приватного):
# Из base64url ключа получить публичный ключ (требует WireGuard)
# base64url → base64 (замена символов + padding) → wg pubkey → base64url
echo "ваш_приватный_ключ=" | tr '_-' '/+' | wg pubkey | basenc --base64url -w0 | tr -d '='Публичный ключ можно безопасно передавать клиентам — с ним можно только зашифровать данные, но не расшифровать.
Клиент (WebSocket) ←→ MSGW ←→ Redis
↑
HTTP QUERY
|
Сервер
Сервер отправляет уведомления через QUERY, все подключённые клиенты получают их мгновенно. Отключившиеся клиенты восстанавливают историю из хранилища.
Сервис A ──QUERY──→ MSGW ←──WS──→ Сервис B
↓
Redis
A отправляет send, B обрабатывает и отвечает done. Если B упал — задача ждёт в хранилище до его восстановления.
Клиент ──POST──→ MSGW ──POST──→ Бэкенд
Бэкенд скрыт за MSGW. Клиент шифрует чувствительные данные публичным ключом → MSGW расшифровывает → бэкенд получает чистый JSON.
Режимы 1 и 2 можно использовать одновременно.
| Переменная | По умолчанию | Описание |
|---|---|---|
MSGW_CACHE_URL |
mem:// |
Хранилище сообщений. mem:// — в памяти (безопасно для тестов), redis://host:port/db — Redis, mongo://host:port/db — MongoDB |
MSGW_CACHE_TTL |
3600 |
Время жизни сообщения в секундах |
MSGW_ECIES_KEY |
(пусто) | Приватный ключ для расшифровки. Без него прокси-режим отключён |
APP |
MSGW |
Префикс переменных окружения (можно изменить, тогда все MSGW_* станут <APP>_*) |
Примечание: Параметры Uvicorn задаются через переменные окружения
UVICORN_*(см. Dockerfile, строки 19–33).fastapi runчитает их автоматически. Переопределить можно через-e UVICORN_WORKERS=4при запуске контейнера — без измененияCMD. CLI-аргументы вCMD(--host,--port,--workers=1) служат значениями по умолчанию и перебиваются переменными окружения.
+-------------+ +----------------+ +-------------------+
| Клиенты | | MessageCenter | | Redis / Backend |
| WS / HTTP |<---->| :8000 |<---->| |
+-------------+ +----------------+ +-------------------+
| |
| Сообщения (WS, QUERY) | Прокси (POST + upstream)
+-----------------------+
Сервис не требует файлов конфигурации — управляется только переменными окружения. Подходит для Docker Compose, Kubernetes, Nomad.