first
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user