// Package api contient tous les handlers HTTP et les middlewares de ProxmoxPanel. package api import ( "context" "encoding/json" "net" "net/http" "strings" "sync" "time" "git.geronzi.fr/proxmoxPanel/core/backend/internal/auth" ) // Clés de contexte pour transmettre les claims JWT aux handlers. type contextKey string const ( ClaimsKey contextKey = "claims" ) // RateLimiter est un simple rate limiter par IP basé sur un token bucket. type RateLimiter struct { mu sync.Mutex buckets map[string]*bucket maxReq int window time.Duration cleanTicker *time.Ticker } type bucket struct { count int resetAt time.Time } // NewRateLimiter crée un rate limiter avec maxReq requêtes par fenêtre temporelle. func NewRateLimiter(maxReq int, window time.Duration) *RateLimiter { rl := &RateLimiter{ buckets: make(map[string]*bucket), maxReq: maxReq, window: window, cleanTicker: time.NewTicker(5 * time.Minute), } go rl.cleanup() return rl } // Allow vérifie si une IP peut effectuer une requête supplémentaire. func (rl *RateLimiter) Allow(ip string) bool { rl.mu.Lock() defer rl.mu.Unlock() b, exists := rl.buckets[ip] if !exists || time.Now().After(b.resetAt) { rl.buckets[ip] = &bucket{count: 1, resetAt: time.Now().Add(rl.window)} return true } if b.count >= rl.maxReq { return false } b.count++ return true } func (rl *RateLimiter) cleanup() { for range rl.cleanTicker.C { rl.mu.Lock() now := time.Now() for ip, b := range rl.buckets { if now.After(b.resetAt) { delete(rl.buckets, ip) } } rl.mu.Unlock() } } // Middleware sécurité : headers HTTP protecteurs. func SecurityHeaders(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-XSS-Protection", "1; mode=block") w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin") // CSP assez souple pour permettre les WebSockets et les assets locaux w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; connect-src 'self' ws: wss:") next.ServeHTTP(w, r) }) } // RequireAuth est le middleware d'authentification JWT. // Il extrait et valide le Bearer token depuis l'en-tête Authorization. func RequireAuth(jwtManager *auth.JWTManager) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tokenStr := extractBearerToken(r) if tokenStr == "" { JSONError(w, "Token d'authentification manquant", http.StatusUnauthorized) return } claims, err := jwtManager.ValidateAccessToken(tokenStr) if err != nil { JSONError(w, "Token invalide ou expiré", http.StatusUnauthorized) return } // Injecter les claims dans le contexte ctx := context.WithValue(r.Context(), ClaimsKey, claims) next.ServeHTTP(w, r.WithContext(ctx)) }) } } // RequireAdmin vérifie que l'utilisateur connecté est administrateur. func RequireAdmin(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { claims := GetClaims(r) if claims == nil || !claims.IsAdmin { JSONError(w, "Accès réservé aux administrateurs", http.StatusForbidden) return } next.ServeHTTP(w, r) }) } // RateLimit crée un middleware de rate limiting pour les endpoints sensibles. func RateLimit(limiter *RateLimiter) func(http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { ip := clientIP(r) if !limiter.Allow(ip) { JSONError(w, "Trop de requêtes, veuillez patienter", http.StatusTooManyRequests) return } next.ServeHTTP(w, r) }) } } // GetClaims extrait les claims JWT du contexte de la requête. func GetClaims(r *http.Request) *auth.Claims { claims, _ := r.Context().Value(ClaimsKey).(*auth.Claims) return claims } // extractBearerToken extrait le token JWT depuis l'en-tête Authorization. func extractBearerToken(r *http.Request) string { header := r.Header.Get("Authorization") if strings.HasPrefix(header, "Bearer ") { return strings.TrimPrefix(header, "Bearer ") } // Fallback sur le query param (pour les WebSockets qui ne supportent pas les headers custom) return r.URL.Query().Get("token") } // clientIP extrait l'IP réelle du client (en tenant compte des proxys). func clientIP(r *http.Request) string { if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" { parts := strings.Split(fwd, ",") return strings.TrimSpace(parts[0]) } if realIP := r.Header.Get("X-Real-IP"); realIP != "" { return realIP } ip, _, err := net.SplitHostPort(r.RemoteAddr) if err != nil { return r.RemoteAddr } return ip } // JSONResponse envoie une réponse JSON avec le code HTTP donné. func JSONResponse(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } // JSONError envoie une réponse d'erreur JSON standardisée. func JSONError(w http.ResponseWriter, message string, status int) { JSONResponse(w, status, map[string]string{"error": message}) }