2025-12-03 19:29:37 +00:00
|
|
|
package middleware
|
|
|
|
|
|
|
|
|
|
import (
|
2025-12-25 14:29:31 +00:00
|
|
|
"fmt"
|
2025-12-03 19:29:37 +00:00
|
|
|
"net/http"
|
2025-12-21 23:55:51 +00:00
|
|
|
"os"
|
2025-12-03 19:29:37 +00:00
|
|
|
"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
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-26 09:58:43 +00:00
|
|
|
// 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",
|
2026-02-27 08:43:25 +00:00
|
|
|
// v0.903: login/register no longer excluded - subject to global rate limit (100 req/min) + endpoint-specific limiters
|
chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 15:43:21 +00:00
|
|
|
// SEC-009, SEC-010: refresh and check-username have EndpointLimiter, not excluded
|
2025-12-26 09:58:43 +00:00
|
|
|
"/api/v1/auth/verify-email",
|
|
|
|
|
"/api/v1/auth/resend-verification",
|
|
|
|
|
"/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
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
// Middleware retourne le middleware Gin pour le rate limiting
|
|
|
|
|
func (rl *SimpleRateLimiter) Middleware() gin.HandlerFunc {
|
|
|
|
|
return func(c *gin.Context) {
|
2025-12-26 09:58:43 +00:00
|
|
|
// Exclure les routes critiques du rate limiting
|
|
|
|
|
if isExcludedPath(c.Request.URL.Path) {
|
|
|
|
|
c.Next()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
chore: consolidate CI, E2E, backend and frontend updates
- CI: workflows updates (cd, ci), remove playwright.yml
- E2E: global-setup, auth/playlists/profile specs
- Remove playwright-report and test-results artifacts from tracking
- Backend: auth, handlers, services, workers, migrations
- Frontend: components, features, vite config
- Add e2e-results.json to gitignore
- Docs: REMEDIATION_PROGRESS, audit archive
- Rust: chat-server, stream-server updates
2026-02-17 15:43:21 +00:00
|
|
|
// SEC-011: Never bypass rate limiting in production
|
|
|
|
|
if os.Getenv("APP_ENV") == "production" {
|
|
|
|
|
// Continue to rate limit
|
|
|
|
|
} else if os.Getenv("DISABLE_RATE_LIMIT_FOR_TESTS") == "true" {
|
2025-12-21 23:55:51 +00:00
|
|
|
c.Next()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-03 19:29:37 +00:00
|
|
|
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()
|
2025-12-25 14:29:31 +00:00
|
|
|
resetTime := now.Add(rl.window).Unix()
|
|
|
|
|
retryAfter := int(rl.window.Seconds())
|
2026-01-13 18:47:57 +00:00
|
|
|
|
2025-12-25 14:29:31 +00:00
|
|
|
// INT-013: Standardize rate limit response format
|
2025-12-03 19:29:37 +00:00
|
|
|
c.Header("X-RateLimit-Limit", strconv.Itoa(rl.limit))
|
|
|
|
|
c.Header("X-RateLimit-Remaining", "0")
|
2025-12-25 14:29:31 +00:00
|
|
|
c.Header("X-RateLimit-Reset", strconv.FormatInt(resetTime, 10))
|
|
|
|
|
c.Header("Retry-After", strconv.Itoa(retryAfter))
|
2025-12-03 19:29:37 +00:00
|
|
|
c.JSON(http.StatusTooManyRequests, gin.H{
|
2025-12-25 14:29:31 +00:00
|
|
|
"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,
|
|
|
|
|
},
|
2025-12-03 19:29:37 +00:00
|
|
|
})
|
|
|
|
|
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)
|
|
|
|
|
}
|