package middleware import ( "net/http" "strings" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // RequestSizeLimit limits the size of request bodies func RequestSizeLimit(maxSize int64) gin.HandlerFunc { return func(c *gin.Context) { // Skip for upload endpoints (they have their own limits) if strings.Contains(c.Request.URL.Path, "/upload") { c.Next() return } c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize) c.Next() } } // SanitizeHeaders removes potentially dangerous headers func SanitizeHeaders() gin.HandlerFunc { return func(c *gin.Context) { // Remove server information leakage c.Writer.Header().Del("Server") c.Writer.Header().Del("X-Powered-By") c.Next() } } // ValidateContentType ensures proper content type for POST/PUT/PATCH func ValidateContentType() gin.HandlerFunc { return func(c *gin.Context) { if c.Request.Method == "POST" || c.Request.Method == "PUT" || c.Request.Method == "PATCH" { contentType := c.GetHeader("Content-Type") path := c.Request.URL.Path // Allow multipart for uploads and image processing crop upload if strings.Contains(path, "/upload") || strings.Contains(path, "/image-processing/crop-upload") || strings.Contains(path, "/admin/scoreboard/qr") || strings.Contains(path, "/admin/scoreboard/load") { c.Next() return } // Allow scoreboard timer control endpoints without requiring JSON body // These actions do not read request body and are triggered via simple POSTs from remote UI if strings.Contains(path, "/admin/scoreboard/timer/") { c.Next() return } if strings.Contains(path, "/admin/scoreboard/swap-sides") || strings.Contains(path, "/admin/scoreboard/second-half") { c.Next() return } if strings.Contains(path, "/rembg/start") { c.Next() return } // Require JSON for other API endpoints if !strings.Contains(contentType, "application/json") { c.JSON(http.StatusUnsupportedMediaType, gin.H{ "error": "Content-Type must be application/json", }) c.Abort() return } } c.Next() } } // RequestID adds a unique request ID for tracing func RequestID() gin.HandlerFunc { return func(c *gin.Context) { requestID := c.GetHeader("X-Request-ID") if requestID == "" { requestID = generateRequestID() } c.Set("request_id", requestID) c.Header("X-Request-ID", requestID) c.Next() } } func generateRequestID() string { return uuid.New().String() } // GetRequestID retrieves the request ID from context func GetRequestID(c *gin.Context) string { if id, exists := c.Get("request_id"); exists { if requestID, ok := id.(string); ok { return requestID } } return "" } // SecurityAuditLog logs security-relevant events type SecurityEvent struct { Type string UserID uint IP string Path string Method string RequestID string Details map[string]interface{} } func LogSecurityEvent(c *gin.Context, eventType string, details map[string]interface{}) { event := SecurityEvent{ Type: eventType, IP: c.ClientIP(), Path: c.Request.URL.Path, Method: c.Request.Method, RequestID: c.GetString("request_id"), Details: details, } if userID, exists := c.Get("user_id"); exists { if uid, ok := userID.(uint); ok { event.UserID = uid } } // Log to your logger // logger.Warn("SECURITY_EVENT: type=%s user_id=%d ip=%s path=%s", // event.Type, event.UserID, event.IP, event.Path) }