@@ -0,0 +1,81 @@
|
||||
// Package httpx : middlewares transverses (sécurité, CORS, rate-limit).
|
||||
package httpx
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
// SecurityHeaders ajoute les en-têtes de sécurité recommandés (OWASP).
|
||||
func SecurityHeaders() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
h := c.Writer.Header()
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("Referrer-Policy", "no-referrer")
|
||||
h.Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'")
|
||||
h.Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// CORS restreint les origines à une allowlist explicite.
|
||||
func CORS(allowed []string) gin.HandlerFunc {
|
||||
set := make(map[string]struct{}, len(allowed))
|
||||
for _, o := range allowed {
|
||||
set[o] = struct{}{}
|
||||
}
|
||||
return func(c *gin.Context) {
|
||||
origin := c.GetHeader("Origin")
|
||||
if _, ok := set[origin]; ok {
|
||||
h := c.Writer.Header()
|
||||
h.Set("Access-Control-Allow-Origin", origin)
|
||||
h.Set("Vary", "Origin")
|
||||
h.Set("Access-Control-Allow-Methods", "GET,POST,PATCH,DELETE,OPTIONS")
|
||||
h.Set("Access-Control-Allow-Headers", "Authorization,Content-Type")
|
||||
}
|
||||
if c.Request.Method == http.MethodOptions {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// ipLimiter : rate-limiter par IP (token bucket).
|
||||
type ipLimiter struct {
|
||||
mu sync.Mutex
|
||||
buck map[string]*rate.Limiter
|
||||
r rate.Limit
|
||||
burst int
|
||||
}
|
||||
|
||||
func newIPLimiter(r rate.Limit, burst int) *ipLimiter {
|
||||
return &ipLimiter{buck: make(map[string]*rate.Limiter), r: r, burst: burst}
|
||||
}
|
||||
|
||||
func (l *ipLimiter) get(ip string) *rate.Limiter {
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
lim, ok := l.buck[ip]
|
||||
if !ok {
|
||||
lim = rate.NewLimiter(l.r, l.burst)
|
||||
l.buck[ip] = lim
|
||||
}
|
||||
return lim
|
||||
}
|
||||
|
||||
// RateLimit limite chaque IP à r req/s avec un burst donné.
|
||||
func RateLimit(perSecond float64, burst int) gin.HandlerFunc {
|
||||
limiter := newIPLimiter(rate.Limit(perSecond), burst)
|
||||
return func(c *gin.Context) {
|
||||
if !limiter.get(c.ClientIP()).Allow() {
|
||||
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "trop de requêtes"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user