package middleware import ( "net/http" "sync" "time" "github.com/gin-gonic/gin" ) type visitor struct { tokens float64 lastSeen time.Time } // RateLimiter implements a per-IP token bucket rate limiter. type RateLimiter struct { mu sync.Mutex visitors map[string]*visitor rate float64 // tokens per second burst int // max tokens } // NewRateLimiter creates a rate limiter. // rate is requests per second, burst is max burst size. func NewRateLimiter(rate float64, burst int) *RateLimiter { rl := &RateLimiter{ visitors: make(map[string]*visitor), rate: rate, burst: burst, } // Cleanup stale entries every 5 minutes go func() { for { time.Sleep(5 * time.Minute) rl.cleanup() } }() return rl } // Limit returns a Gin middleware that enforces the rate limit. func (rl *RateLimiter) Limit() gin.HandlerFunc { return func(c *gin.Context) { ip := c.ClientIP() rl.mu.Lock() v, exists := rl.visitors[ip] now := time.Now() if !exists { v = &visitor{tokens: float64(rl.burst), lastSeen: now} rl.visitors[ip] = v } // Refill tokens based on elapsed time elapsed := now.Sub(v.lastSeen).Seconds() v.tokens += elapsed * rl.rate if v.tokens > float64(rl.burst) { v.tokens = float64(rl.burst) } v.lastSeen = now if v.tokens < 1 { rl.mu.Unlock() c.Header("Retry-After", "1") c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ "error": "rate limit exceeded", }) return } v.tokens-- rl.mu.Unlock() c.Next() } } func (rl *RateLimiter) cleanup() { rl.mu.Lock() defer rl.mu.Unlock() cutoff := time.Now().Add(-10 * time.Minute) for ip, v := range rl.visitors { if v.lastSeen.Before(cutoff) { delete(rl.visitors, ip) } } }