package middleware import ( "fmt" "net/http" "os" "strconv" "sync" "time" "github.com/gin-gonic/gin" ) // SimpleRateLimiter est un rate limiter simple basé sur une sliding window en mémoire // Utilisé pour le rate limiting basique par IP sans dépendance Redis type SimpleRateLimiter struct { requests map[string][]time.Time limit int window time.Duration mu sync.Mutex stop chan struct{} // Channel to signal cleanup goroutine to stop } // NewSimpleRateLimiter crée un nouveau rate limiter simple // limit: nombre maximum de requêtes // window: fenêtre de temps (ex: 1 * time.Minute pour 100 req/min) func NewSimpleRateLimiter(limit int, window time.Duration) *SimpleRateLimiter { rl := &SimpleRateLimiter{ requests: make(map[string][]time.Time), limit: limit, window: window, stop: make(chan struct{}), // Initialize the stop channel } // Démarrer la goroutine de nettoyage go rl.cleanup() return rl } // Routes exclues du rate limiting (routes critiques) var excludedRateLimitPaths = []string{ "/health", "/healthz", "/readyz", "/api/v1/health", "/api/v1/healthz", "/api/v1/readyz", "/api/v1/csrf-token", "/api/v1/auth/register", "/api/v1/auth/login", "/api/v1/auth/refresh", "/api/v1/auth/verify-email", "/api/v1/auth/resend-verification", "/api/v1/auth/check-username", "/swagger", "/docs", } // isExcludedPath vérifie si un chemin est exclu du rate limiting func isExcludedPath(path string) bool { for _, excluded := range excludedRateLimitPaths { if path == excluded || (len(path) > len(excluded) && path[:len(excluded)] == excluded) { return true } } return false } // Middleware retourne le middleware Gin pour le rate limiting func (rl *SimpleRateLimiter) Middleware() gin.HandlerFunc { return func(c *gin.Context) { // Exclure les routes critiques du rate limiting if isExcludedPath(c.Request.URL.Path) { c.Next() return } // DÉSACTIVER le rate limiting en mode test/e2e/development pour les tests E2E et développement if c.GetHeader("X-Test-Mode") == "true" || c.GetHeader("X-E2E-Test") == "true" || os.Getenv("NODE_ENV") == "test" || os.Getenv("NODE_ENV") == "e2e" || os.Getenv("NODE_ENV") == "development" || os.Getenv("APP_ENV") == "test" || os.Getenv("APP_ENV") == "e2e" || os.Getenv("APP_ENV") == "development" { c.Next() return } ip := c.ClientIP() rl.mu.Lock() now := time.Now() cutoff := now.Add(-rl.window) // Nettoyer les anciennes requêtes valid := []time.Time{} for _, t := range rl.requests[ip] { if t.After(cutoff) { valid = append(valid, t) } } // Vérifier si la limite est atteinte if len(valid) >= rl.limit { rl.mu.Unlock() resetTime := now.Add(rl.window).Unix() retryAfter := int(rl.window.Seconds()) // INT-013: Standardize rate limit response format c.Header("X-RateLimit-Limit", strconv.Itoa(rl.limit)) c.Header("X-RateLimit-Remaining", "0") c.Header("X-RateLimit-Reset", strconv.FormatInt(resetTime, 10)) c.Header("Retry-After", strconv.Itoa(retryAfter)) c.JSON(http.StatusTooManyRequests, gin.H{ "success": false, "error": gin.H{ "code": 429, "message": "Rate limit exceeded. Please try again later.", "details": []gin.H{ { "field": "rate_limit", "message": fmt.Sprintf("You have exceeded the rate limit of %d requests per %v", rl.limit, rl.window), }, }, "retry_after": retryAfter, "limit": rl.limit, "remaining": 0, "reset": resetTime, }, }) c.Abort() return } // Ajouter la nouvelle requête valid = append(valid, now) rl.requests[ip] = valid remaining := rl.limit - len(valid) rl.mu.Unlock() // Ajouter les headers de rate limiting c.Header("X-RateLimit-Limit", strconv.Itoa(rl.limit)) c.Header("X-RateLimit-Remaining", strconv.Itoa(remaining)) c.Header("X-RateLimit-Reset", strconv.FormatInt(now.Add(rl.window).Unix(), 10)) c.Next() } } // UpdateLimits met à jour les limites de rate limiting (T0034) // Permet le rechargement à chaud des limites sans redémarrer l'application func (rl *SimpleRateLimiter) UpdateLimits(limit int, window time.Duration) { rl.mu.Lock() defer rl.mu.Unlock() rl.limit = limit rl.window = window } // cleanup nettoie périodiquement les anciennes requêtes func (rl *SimpleRateLimiter) cleanup() { ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() // Ensure ticker is stopped for { select { case <-ticker.C: rl.mu.Lock() cutoff := time.Now().Add(-rl.window) for ip, times := range rl.requests { valid := []time.Time{} for _, t := range times { if t.After(cutoff) { valid = append(valid, t) } } if len(valid) == 0 { delete(rl.requests, ip) } else { rl.requests[ip] = valid } } rl.mu.Unlock() case <-rl.stop: // Listen for stop signal return // Exit goroutine } } } // Stop signale au goroutine de nettoyage de s'arrêter func (rl *SimpleRateLimiter) Stop() { close(rl.stop) }