package middleware import ( "net/http" "os" "strconv" "strings" "github.com/gin-gonic/gin" ) // InputGateConfig holds configuration for input validation. type InputGateConfig struct { // Maximum request body size (default: 10MB) MaxBodySize int64 // Maximum file upload size (default: 50MB) MaxFileSize int64 // Allowed content types AllowedContentTypes map[string]bool // Allowed file types for uploads AllowedFileTypes map[string]bool // Blocked file extensions BlockedExtensions map[string]bool // Paths that allow larger uploads LargeUploadPaths []string // Paths excluded from validation ExcludedPaths []string // Enable strict content type checking StrictContentType bool } // DefaultInputGateConfig returns sensible default configuration. func DefaultInputGateConfig() InputGateConfig { maxSize := int64(10 * 1024 * 1024) // 10MB if envSize := os.Getenv("MAX_REQUEST_BODY_SIZE"); envSize != "" { if size, err := strconv.ParseInt(envSize, 10, 64); err == nil { maxSize = size } } return InputGateConfig{ MaxBodySize: maxSize, MaxFileSize: 50 * 1024 * 1024, // 50MB AllowedContentTypes: map[string]bool{ "application/json": true, "application/x-www-form-urlencoded": true, "multipart/form-data": true, "text/plain": true, }, AllowedFileTypes: map[string]bool{ "image/jpeg": true, "image/png": true, "image/gif": true, "image/webp": true, "application/pdf": true, "text/csv": true, "application/msword": true, "application/vnd.openxmlformats-officedocument.wordprocessingml.document": true, "application/vnd.ms-excel": true, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet": true, }, BlockedExtensions: map[string]bool{ ".exe": true, ".bat": true, ".cmd": true, ".com": true, ".msi": true, ".dll": true, ".scr": true, ".pif": true, ".vbs": true, ".js": true, ".jar": true, ".sh": true, ".ps1": true, ".app": true, }, LargeUploadPaths: []string{ "/api/v1/files/upload", "/api/v1/documents/upload", "/api/v1/attachments", }, ExcludedPaths: []string{ "/health", "/metrics", "/api/v1/health", }, StrictContentType: true, } } // isExcludedPath checks if path is excluded from validation. func (c *InputGateConfig) isExcludedPath(path string) bool { for _, excluded := range c.ExcludedPaths { if path == excluded { return true } } return false } // isLargeUploadPath checks if path allows larger uploads. func (c *InputGateConfig) isLargeUploadPath(path string) bool { for _, uploadPath := range c.LargeUploadPaths { if strings.HasPrefix(path, uploadPath) { return true } } return false } // getMaxSize returns the maximum allowed body size for the path. func (c *InputGateConfig) getMaxSize(path string) int64 { if c.isLargeUploadPath(path) { return c.MaxFileSize } return c.MaxBodySize } // validateContentType validates the content type. func (c *InputGateConfig) validateContentType(contentType string) (bool, string) { if contentType == "" { return true, "" } // Extract base content type (remove charset, boundary, etc.) baseType := strings.Split(contentType, ";")[0] baseType = strings.TrimSpace(strings.ToLower(baseType)) if !c.AllowedContentTypes[baseType] { return false, "Content-Type '" + baseType + "' is not allowed" } return true, "" } // hasBlockedExtension checks if filename has a blocked extension. func (c *InputGateConfig) hasBlockedExtension(filename string) bool { if filename == "" { return false } lowerFilename := strings.ToLower(filename) for ext := range c.BlockedExtensions { if strings.HasSuffix(lowerFilename, ext) { return true } } return false } // InputGate returns a middleware that validates incoming request bodies. // // Usage: // // r.Use(middleware.InputGate()) // // // Or with custom config: // config := middleware.DefaultInputGateConfig() // config.MaxBodySize = 5 * 1024 * 1024 // 5MB // r.Use(middleware.InputGateWithConfig(config)) func InputGate() gin.HandlerFunc { return InputGateWithConfig(DefaultInputGateConfig()) } // InputGateWithConfig returns an input gate middleware with custom configuration. func InputGateWithConfig(config InputGateConfig) gin.HandlerFunc { return func(c *gin.Context) { // Skip excluded paths if config.isExcludedPath(c.Request.URL.Path) { c.Next() return } // Skip validation for GET, HEAD, OPTIONS requests method := c.Request.Method if method == "GET" || method == "HEAD" || method == "OPTIONS" { c.Next() return } // Validate content type for requests with body contentType := c.GetHeader("Content-Type") if config.StrictContentType { valid, errMsg := config.validateContentType(contentType) if !valid { c.AbortWithStatusJSON(http.StatusUnsupportedMediaType, gin.H{ "error": "unsupported_media_type", "message": errMsg, }) return } } // Check Content-Length header contentLength := c.GetHeader("Content-Length") if contentLength != "" { length, err := strconv.ParseInt(contentLength, 10, 64) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{ "error": "invalid_content_length", "message": "Invalid Content-Length header", }) return } maxSize := config.getMaxSize(c.Request.URL.Path) if length > maxSize { c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{ "error": "payload_too_large", "message": "Request body exceeds maximum size", "max_size": maxSize, }) return } } // Set max multipart memory for file uploads if strings.Contains(contentType, "multipart/form-data") { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, config.MaxFileSize) } c.Next() } } // ValidateFileUpload validates a file upload. // Use this in upload handlers for detailed validation. func ValidateFileUpload(filename, contentType string, size int64, config *InputGateConfig) (bool, string) { if config == nil { defaultConfig := DefaultInputGateConfig() config = &defaultConfig } // Check size if size > config.MaxFileSize { return false, "File size exceeds maximum allowed" } // Check extension if config.hasBlockedExtension(filename) { return false, "File extension is not allowed" } // Check content type if contentType != "" && !config.AllowedFileTypes[contentType] { return false, "File type '" + contentType + "' is not allowed" } return true, "" }