package middleware import ( "net/http" "sync" "time" "github.com/gin-gonic/gin" ) // ipBucket is a per-IP sliding-window counter. type ipBucket struct { mu sync.Mutex times []time.Time limit int window time.Duration } func (b *ipBucket) allow() bool { b.mu.Lock() defer b.mu.Unlock() now := time.Now() cutoff := now.Add(-b.window) // Evict timestamps outside the window valid := b.times[:0] for _, t := range b.times { if t.After(cutoff) { valid = append(valid, t) } } b.times = valid if len(b.times) >= b.limit { return false } b.times = append(b.times, now) return true } // RateLimiter is a per-IP sliding-window rate limiter backed by sync.Map. // It is safe for concurrent use and cleans up idle buckets automatically. type RateLimiter struct { buckets sync.Map // string(ip) → *ipBucket limit int window time.Duration } // NewRateLimiter creates a limiter that allows up to limit requests per window // per IP address. func NewRateLimiter(limit int, window time.Duration) *RateLimiter { rl := &RateLimiter{limit: limit, window: window} go rl.gc() return rl } // gc periodically removes buckets that have had no activity for one full window. func (rl *RateLimiter) gc() { ticker := time.NewTicker(rl.window) defer ticker.Stop() for range ticker.C { rl.buckets.Range(func(k, v any) bool { b := v.(*ipBucket) b.mu.Lock() if len(b.times) == 0 { rl.buckets.Delete(k) } b.mu.Unlock() return true }) } } // Middleware returns a gin.HandlerFunc that enforces the rate limit. func (rl *RateLimiter) Middleware() gin.HandlerFunc { return func(c *gin.Context) { ip := c.ClientIP() v, _ := rl.buckets.LoadOrStore(ip, &ipBucket{ times: make([]time.Time, 0, rl.limit), limit: rl.limit, window: rl.window, }) b := v.(*ipBucket) if !b.allow() { c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{ "code": "rate_limited", "message": "too many requests — slow down", }) return } c.Next() } }