This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/consent-service/internal/middleware/input_gate.go
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

248 lines
6.4 KiB
Go

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, ""
}