Skip to content

Repository files navigation

MessageCenter (MSGW)

MessageCenter — это всегда тот же сервис, в коде и переменных он называется MSGW.

Лёгкий сервис для обмена сообщениями и безопасной проксировки запросов. Запускается одним контейнером, настраивается переменными окружения.

Для чего это нужно

Представьте, что у вас есть несколько сервисов, которым нужно обмениваться данными. MSGW решает две задачи:

  1. Шина сообщений — как чат с историей: отправитель кладёт сообщение, все подключённые получатели получают его мгновенно. Если получатель был отключён — сообщение сохранится в хранилище и дойдёт, когда он подключится.
  2. Безопасный прокси — как почтовый ящик, который вскрывает зашифрованный конверт и доставляет содержимое адресату. Вы отправляете 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=... Пересылка запросов на бэкенд с расшифровкой (нужен ключ)

Режим 1: Шина сообщений

Как это работает

  Отправитель   ──┐
                  ├──→ 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).

Подключение через WebSocket

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))

Отправка через HTTP QUERY

Если у вас нет 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 — сообщение принято, сохранено и разослано.


Режим 2: Прокси с расшифровкой

Включается только при заданной переменной MSGW_ECIES_KEY. Без ключа этот режим не работает.

Зачем это нужно

У вас есть бэкенд, который не должен быть доступен напрямую. MSGW стоит перед ним и:

  1. Принимает POST-запросы
  2. Расшифровывает зашифрованные фрагменты в теле запроса
  3. Пересылает чистый запрос на бэкенд

«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)
  1. Клиент отправляет POST на любой путь MSGW, например /notify.
  2. В URL указывает ?upstream=http://backend:8080/notify — куда переслать.
  3. В теле JSON зашифрованные секции оборачиваются в {{...}}. Внутри — base64url-строка длиной ≥ 43 символа ([A-Za-z0-9_-]).
  4. MSGW находит все {{...}}, расшифровывает их своим приватным ключом и подставляет открытые значения.
  5. Полученный чистый 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 сканирует тело запроса, находит все {{...}}, расшифровывает и подставляет. Если расшифровка не удалась — фрагмент остаётся как есть, запрос всё равно уходит на бэкенд.

Query-параметры

  • Все параметры, кроме 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 '='

Публичный ключ можно безопасно передавать клиентам — с ним можно только зашифровать данные, но не расшифровать.


Сценарии использования

1. Чат / уведомления в реальном времени

  Клиент (WebSocket) ←→ MSGW ←→ Redis
                          ↑
                    HTTP QUERY
                          |
                       Сервер

Сервер отправляет уведомления через QUERY, все подключённые клиенты получают их мгновенно. Отключившиеся клиенты восстанавливают историю из хранилища.

2. Микросервисная шина с подтверждениями

  Сервис A ──QUERY──→ MSGW ←──WS──→ Сервис B
                            ↓
                          Redis

A отправляет send, B обрабатывает и отвечает done. Если B упал — задача ждёт в хранилище до его восстановления.

3. Безопасный прокси для бэкенда

  Клиент ──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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Packages

Contributors

Languages