package middleware import ( "net/http" "sync" "time" "github.com/gin-gonic/gin" "golang.org/x/time/rate" ) // PerIPRateLimiter limits requests per client IP, intended for sensitive // low-frequency endpoints like login (defense against credential stuffing // / brute force). type PerIPRateLimiter struct { mu sync.Mutex limiters map[string]*rate.Limiter r rate.Limit burst int } func NewPerIPRateLimiter(requestsPerMinute int, burst int) *PerIPRateLimiter { l := &PerIPRateLimiter{ limiters: make(map[string]*rate.Limiter), r: rate.Every(time.Minute / time.Duration(requestsPerMinute)), burst: burst, } go l.cleanupLoop() return l } func (l *PerIPRateLimiter) getLimiter(ip string) *rate.Limiter { l.mu.Lock() defer l.mu.Unlock() limiter, exists := l.limiters[ip] if !exists { limiter = rate.NewLimiter(l.r, l.burst) l.limiters[ip] = limiter } return limiter } func (l *PerIPRateLimiter) cleanupLoop() { for { time.Sleep(10 * time.Minute) l.mu.Lock() for ip, limiter := range l.limiters { if limiter.Tokens() >= float64(l.burst) { delete(l.limiters, ip) } } l.mu.Unlock() } } func (l *PerIPRateLimiter) Middleware() gin.HandlerFunc { return func(c *gin.Context) { ip := c.ClientIP() if !l.getLimiter(ip).Allow() { c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "too many requests, try again later"}) return } c.Next() } }