veza/veza-backend-api/internal/handlers/chat_websocket_handler.go
senke a0a611525c fix(v0.12.6.1): remediate remaining 15 MEDIUM + LOW pentest findings
MEDIUM-002: Remove manual X-Forwarded-For parsing in metrics_protection.go,
  use c.ClientIP() only (respects SetTrustedProxies)
MEDIUM-003: Pin ClamAV Docker image to 1.4 across all compose files
MEDIUM-004: Add clampLimit(100) to 15+ handlers that parsed limit directly
MEDIUM-006: Remove unsafe-eval from CSP script-src on Swagger routes
MEDIUM-007: Pin all GitHub Actions to SHA in 11 workflow files
MEDIUM-008: Replace rabbitmq:3-management-alpine with rabbitmq:3-alpine in prod
MEDIUM-009: Add trial-already-used check in subscription service
MEDIUM-010: Add 60s periodic token re-validation to WebSocket connections
MEDIUM-011: Mask email in auth handler logs with maskEmail() helper
MEDIUM-012: Add k-anonymity threshold (k=5) to playback analytics stats
LOW-001: Align frontend password policy to 12 chars (matching backend)
LOW-003: Replace deprecated dotenv with dotenvy crate in Rust stream server
LOW-004: Enable xpack.security in Elasticsearch dev/local compose files
LOW-005: Accept context.Context in CleanupExpiredSessions instead of Background()
LOW-002: Noted — Hyperswitch version update deferred (requires payment integration tests)

29/30 findings remediated. 1 noted (LOW-002).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 06:13:38 +01:00

89 lines
2.2 KiB
Go

package handlers
import (
"time"
"github.com/coder/websocket"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
apperrors "veza-backend-api/internal/errors"
"veza-backend-api/internal/services"
chatws "veza-backend-api/internal/websocket/chat"
)
type ChatWebSocketHandler struct {
chatService *services.ChatService
hub *chatws.Hub
handler *chatws.MessageHandler
logger *zap.Logger
}
func NewChatWebSocketHandler(chatService *services.ChatService, hub *chatws.Hub, handler *chatws.MessageHandler, logger *zap.Logger) *ChatWebSocketHandler {
if logger == nil {
logger = zap.NewNop()
}
return &ChatWebSocketHandler{
chatService: chatService,
hub: hub,
handler: handler,
logger: logger,
}
}
func (h *ChatWebSocketHandler) HandleWebSocket(c *gin.Context) {
tokenString := c.Query("token")
if tokenString == "" {
RespondWithAppError(c, apperrors.NewUnauthorizedError("missing token"))
return
}
claims, err := h.chatService.ValidateChatToken(tokenString)
if err != nil {
h.logger.Warn("Invalid chat token",
zap.Error(err))
RespondWithAppError(c, apperrors.NewUnauthorizedError("invalid or expired token"))
return
}
conn, err := websocket.Accept(c.Writer, c.Request, &websocket.AcceptOptions{
InsecureSkipVerify: true,
})
if err != nil {
h.logger.Error("Failed to accept WebSocket",
zap.Error(err),
zap.String("user_id", claims.UserID.String()))
return
}
client := chatws.NewClient(h.hub, conn, claims.UserID, claims.Username, h.handler, h.logger)
h.hub.Register(client)
client.SendJSON(chatws.NewActionConfirmedResponse("connected", true))
ctx := c.Request.Context()
// SECURITY(MEDIUM-010): Periodic token re-validation.
// Re-validate every 60s; close connection if token is expired or revoked.
go func() {
ticker := time.NewTicker(60 * time.Second)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
return
case <-ticker.C:
if _, err := h.chatService.ValidateChatToken(tokenString); err != nil {
h.logger.Info("WebSocket token expired, closing connection",
zap.String("user_id", claims.UserID.String()))
conn.Close(websocket.StatusPolicyViolation, "token expired")
return
}
}
}
}()
go client.WritePump(ctx)
go client.ReadPump(ctx)
}