veza/veza-backend-api/internal/middleware/ratelimit.go
2025-12-03 20:29:37 +01:00

126 lines
3.2 KiB
Go

package middleware
import (
"net/http"
"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
}
// Middleware retourne le middleware Gin pour le rate limiting
func (rl *SimpleRateLimiter) Middleware() gin.HandlerFunc {
return func(c *gin.Context) {
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()
c.Header("X-RateLimit-Limit", strconv.Itoa(rl.limit))
c.Header("X-RateLimit-Remaining", "0")
c.Header("X-RateLimit-Reset", strconv.FormatInt(now.Add(rl.window).Unix(), 10))
c.JSON(http.StatusTooManyRequests, gin.H{
"error": "Rate limit exceeded",
"retry_after": int(rl.window.Seconds()),
})
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)
}