This repository has been archived on 2026-04-03. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
core/server/middleware/ratelimit.go
2026-02-15 23:41:48 +00:00

93 lines
1.7 KiB
Go

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)
}
}
}