veza/veza-backend-api/internal/handlers/chat_websocket_handler.go

90 lines
2.3 KiB
Go
Raw Normal View History

package handlers
import (
"time"
"github.com/coder/websocket"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
2026-03-06 18:13:16 +00:00
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 == "" {
2026-03-06 18:13:16 +00:00
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))
2026-03-06 18:13:16 +00:00
RespondWithAppError(c, apperrors.NewUnauthorizedError("invalid or expired token"))
return
}
conn, err := websocket.Accept(c.Writer, c.Request, &websocket.AcceptOptions{
fix(v0.12.6): apply all pentest remediations — 36 findings across 36 files CRITICAL fixes: - Race condition (TOCTOU) in payout/refund with SELECT FOR UPDATE (CRITICAL-001/002) - IDOR on analytics endpoint — ownership check enforced (CRITICAL-003) - CSWSH on all WebSocket endpoints — origin whitelist (CRITICAL-004) - Mass assignment on user self-update — strip privileged fields (CRITICAL-005) HIGH fixes: - Path traversal in marketplace upload — UUID filenames (HIGH-001) - IP spoofing — use Gin trusted proxy c.ClientIP() (HIGH-002) - Popularity metrics (followers, likes) set to json:"-" (HIGH-003) - bcrypt cost hardened to 12 everywhere (HIGH-004) - Refresh token lock made mandatory (HIGH-005) - Stream token replay prevention with access_count (HIGH-006) - Subscription trial race condition fixed (HIGH-007) - License download expiration check (HIGH-008) - Webhook amount validation (HIGH-009) - pprof endpoint removed from production (HIGH-010) MEDIUM fixes: - WebSocket message size limit 64KB (MEDIUM-010) - HSTS header in nginx production (MEDIUM-001) - CORS origin restricted in nginx-rtmp (MEDIUM-002) - Docker alpine pinned to 3.21 (MEDIUM-003/004) - Redis authentication enforced (MEDIUM-005) - GDPR account deletion expanded (MEDIUM-006) - .gitignore hardened (MEDIUM-007) LOW/INFO fixes: - GitHub Actions SHA pinning on all workflows (LOW-001) - .env.example security documentation (INFO-001) - Production CORS set to HTTPS (LOW-002) All tests pass. Go and Rust compile clean. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 23:44:46 +00:00
OriginPatterns: GetAllowedWebSocketOrigins(),
})
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))
2026-03-06 18:13:16 +00:00
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)
}