80 lines
2.2 KiB
Go
80 lines
2.2 KiB
Go
package main
|
|
|
|
import (
|
|
"net"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// rateLimiter autorise au plus une action par IP dans une fenetre glissante.
|
|
// Pas de token bucket : pour un endpoint de creation de session, "1 par
|
|
// fenetre" est largement suffisant et plus simple a raisonner.
|
|
type rateLimiter struct {
|
|
mu sync.Mutex
|
|
lastSeen map[string]time.Time
|
|
window time.Duration
|
|
}
|
|
|
|
func newRateLimiter(window time.Duration) *rateLimiter {
|
|
rl := &rateLimiter{
|
|
lastSeen: make(map[string]time.Time),
|
|
window: window,
|
|
}
|
|
go rl.cleanupLoop()
|
|
return rl
|
|
}
|
|
|
|
// Allow renvoie true si l'IP n'a pas deja declenche d'action dans la fenetre.
|
|
// Sur true, l'horloge de l'IP est reinitialisee.
|
|
func (rl *rateLimiter) Allow(ip string) bool {
|
|
rl.mu.Lock()
|
|
defer rl.mu.Unlock()
|
|
now := time.Now()
|
|
if last, ok := rl.lastSeen[ip]; ok && now.Sub(last) < rl.window {
|
|
return false
|
|
}
|
|
rl.lastSeen[ip] = now
|
|
return true
|
|
}
|
|
|
|
// cleanupLoop purge les entrees plus anciennes que 2x la fenetre pour eviter
|
|
// la croissance non bornee de la map sous trafic varie.
|
|
func (rl *rateLimiter) cleanupLoop() {
|
|
ticker := time.NewTicker(5 * time.Minute)
|
|
defer ticker.Stop()
|
|
for range ticker.C {
|
|
cutoff := time.Now().Add(-2 * rl.window)
|
|
rl.mu.Lock()
|
|
for ip, t := range rl.lastSeen {
|
|
if t.Before(cutoff) {
|
|
delete(rl.lastSeen, ip)
|
|
}
|
|
}
|
|
rl.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// clientIP extrait l'IP reelle du visiteur en tenant compte du setup reverse-proxy.
|
|
// Ordre de priorite :
|
|
// 1. CF-Connecting-IP : defini par Cloudflare sur la base de SA propre vue du
|
|
// peer TCP, non-forgeable par le client, ecrase toute valeur entrante.
|
|
// 2. X-Forwarded-For, derniere entree : quand seul Traefik est en front (pas
|
|
// de Cloudflare), Traefik append l'IP qu'il observe. Prendre la premiere
|
|
// serait une faille (header forgeable).
|
|
// 3. RemoteAddr : fallback si aucun header de proxy n'est present.
|
|
func clientIP(r *http.Request) string {
|
|
if cfIP := r.Header.Get("CF-Connecting-IP"); cfIP != "" {
|
|
return strings.TrimSpace(cfIP)
|
|
}
|
|
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
|
|
parts := strings.Split(xff, ",")
|
|
return strings.TrimSpace(parts[len(parts)-1])
|
|
}
|
|
if host, _, err := net.SplitHostPort(r.RemoteAddr); err == nil {
|
|
return host
|
|
}
|
|
return r.RemoteAddr
|
|
}
|