feat(gateway): WebSocket Gateway с HELLO, IDENTIFY и READY
Реализован Gateway по AGENT.md §8.3:
- оп-коды 0/1/2/3/4/7/10/11, единый конверт {op,t,d,s};
- HELLO с интервалом сердцебиения (45 с) и идентификатором сессии;
- IDENTIFY/RESUME с проверкой токена через auth.ResolveSession;
- READY: пользователь, серверы, видимые комнаты, роли, участники и права,
собранные через движок прав (VIEW_CHANNEL-фильтрация);
- HEARTBEAT → HEARTBEAT_ACK, лимит частоты IDENTIFY (5 с);
- RESUME: буфер на 1000 событий с TTL 5 минут, догон пропущенных событий и
подтверждение RESUMED, полный READY при расхождении состояния;
- рассылка Dispatch/SendToUser с буферизацией для отключённых сессий;
- проверка Origin по allowlist доменов инстанса (AGENT.md §9.7);
- httpx.statusRecorder пробрасывает Hijack, иначе апгрейд ломается.
Тесты: 6 сценариев Gateway (READY, скрытые комнаты, неверный токен,
heartbeat, dispatch, RESUME) и 2 теста маршрута /gateway через полный стек
middleware, включая отклонение чужого Origin.
This commit is contained in:
+27
-16
@@ -14,6 +14,7 @@ import (
|
||||
"glchat/internal/auth"
|
||||
"glchat/internal/config"
|
||||
"glchat/internal/database"
|
||||
"glchat/internal/gateway"
|
||||
"glchat/internal/httpx"
|
||||
"glchat/internal/meta"
|
||||
"glchat/internal/store"
|
||||
@@ -22,29 +23,32 @@ import (
|
||||
// Deps — зависимости HTTP-слоя: хранилище и сервис аутентификации.
|
||||
// В Фазе 0 они могут отсутствовать (инстанс без секретов ещё поднимается).
|
||||
type Deps struct {
|
||||
Store *store.Store
|
||||
Auth *auth.Service
|
||||
Store *store.Store
|
||||
Auth *auth.Service
|
||||
Gateway *gateway.Service
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
cfg config.Config
|
||||
db *database.DB
|
||||
store *store.Store
|
||||
auth *auth.Service
|
||||
logger *slog.Logger
|
||||
http *http.Server
|
||||
static *staticHandler
|
||||
api huma.API
|
||||
cfg config.Config
|
||||
db *database.DB
|
||||
store *store.Store
|
||||
auth *auth.Service
|
||||
gateway *gateway.Service
|
||||
logger *slog.Logger
|
||||
http *http.Server
|
||||
static *staticHandler
|
||||
api huma.API
|
||||
}
|
||||
|
||||
func New(cfg config.Config, db *database.DB, logger *slog.Logger, deps Deps) *Server {
|
||||
s := &Server{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
store: deps.Store,
|
||||
auth: deps.Auth,
|
||||
logger: logger,
|
||||
static: newStaticHandler(cfg.WebRoot),
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
store: deps.Store,
|
||||
auth: deps.Auth,
|
||||
gateway: deps.Gateway,
|
||||
logger: logger,
|
||||
static: newStaticHandler(cfg.WebRoot),
|
||||
}
|
||||
|
||||
router := chi.NewRouter()
|
||||
@@ -95,6 +99,13 @@ func (s *Server) registerAPI(router chi.Router) huma.API {
|
||||
func (s *Server) registerRoutes(router chi.Router) {
|
||||
router.Get("/healthz", s.handleHealthz)
|
||||
router.Get("/readyz", s.handleReadyz)
|
||||
if s.gateway != nil {
|
||||
router.Handle("/gateway", s.gateway.Handler())
|
||||
} else {
|
||||
router.HandleFunc("/gateway", func(w http.ResponseWriter, _ *http.Request) {
|
||||
writeAPIError(w, auth.ErrSessionExpired)
|
||||
})
|
||||
}
|
||||
|
||||
router.NotFound(s.handleFallback)
|
||||
router.MethodNotAllowed(func(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user