82 lines
2.1 KiB
Go
82 lines
2.1 KiB
Go
// 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()
|
|
}
|
|
}
|