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:
@@ -9,10 +9,14 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/coder/websocket"
|
||||
|
||||
"glchat/internal/auth"
|
||||
"glchat/internal/config"
|
||||
"glchat/internal/database"
|
||||
"glchat/internal/gateway"
|
||||
"glchat/internal/store"
|
||||
)
|
||||
|
||||
@@ -58,7 +62,64 @@ func newTestServer(t *testing.T) (*Server, *database.DB) {
|
||||
if err != nil {
|
||||
t.Fatalf("initialize authentication: %v", err)
|
||||
}
|
||||
return New(cfg, db, logger, Deps{Store: st, Auth: authService}), db
|
||||
gatewayService := gateway.New(st, authService, gateway.NewSnapshot(st), logger, cfg.AllowedOrigins())
|
||||
return New(cfg, db, logger, Deps{Store: st, Auth: authService, Gateway: gatewayService}), db
|
||||
}
|
||||
|
||||
// TestGatewayRouteUpgrades проверяет связку: маршрут /gateway доступен через
|
||||
// полный стек middleware, HELLO приходит до IDENTIFY (AGENT.md 8.3).
|
||||
func TestGatewayRouteUpgrades(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
httpServer := httptest.NewServer(srv.Handler())
|
||||
t.Cleanup(httpServer.Close)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
url := "ws" + strings.TrimPrefix(httpServer.URL, "http") + "/gateway"
|
||||
// coder/websocket закрывает тело ответа сам (dial.go: "You never need to
|
||||
// close resp.Body yourself") — отсюда nolint:bodyclose.
|
||||
conn, _, err := websocket.Dial(ctx, url, &websocket.DialOptions{ //nolint:bodyclose // тело закрывает библиотека
|
||||
HTTPHeader: map[string][]string{"Origin": {"https://gl.mhspx.su"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("dial /gateway: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = conn.CloseNow() })
|
||||
|
||||
_, data, err := conn.Read(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("read HELLO: %v", err)
|
||||
}
|
||||
var envelope struct {
|
||||
Op int `json:"op"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &envelope); err != nil {
|
||||
t.Fatalf("decode HELLO: %v", err)
|
||||
}
|
||||
if envelope.Op != gateway.OpHello {
|
||||
t.Fatalf("op = %d, want HELLO (%d)", envelope.Op, gateway.OpHello)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGatewayRejectsForeignOrigin: подключение с чужого домена отклоняется
|
||||
// (AGENT.md 9.7).
|
||||
func TestGatewayRejectsForeignOrigin(t *testing.T) {
|
||||
srv, _ := newTestServer(t)
|
||||
httpServer := httptest.NewServer(srv.Handler())
|
||||
t.Cleanup(httpServer.Close)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
url := "ws" + strings.TrimPrefix(httpServer.URL, "http") + "/gateway"
|
||||
conn, _, err := websocket.Dial(ctx, url, &websocket.DialOptions{ //nolint:bodyclose // тело закрывает библиотека
|
||||
HTTPHeader: map[string][]string{"Origin": {"https://evil.example"}},
|
||||
})
|
||||
if err == nil {
|
||||
_ = conn.CloseNow()
|
||||
t.Fatal("connection from a foreign origin must be rejected")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHealthz(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user