diff --git a/backend/mw/ratelimit.go b/backend/mw/ratelimit.go new file mode 100644 index 0000000..41f5618 --- /dev/null +++ b/backend/mw/ratelimit.go @@ -0,0 +1,95 @@ +package mw + +import ( + "net/http" + "sync" + "time" + + "github.com/go-chi/chi/v5" +) + +// RateLimiter implements a simple in-memory rate limiter +type RateLimiter struct { + requests map[string][]time.Time + mu sync.RWMutex + limit int + window time.Duration +} + +func NewRateLimiter(limit int, window time.Duration) *RateLimiter { + rl := &RateLimiter{ + requests: make(map[string][]time.Time), + limit: limit, + window: window, + } + // Cleanup old entries periodically + go func() { + for { + time.Sleep(window) + rl.cleanup() + } + }() + return rl +} + +func (rl *RateLimiter) cleanup() { + rl.mu.Lock() + defer rl.mu.Unlock() + now := time.Now() + for key, times := range rl.requests { + var valid []time.Time + for _, t := range times { + if now.Sub(t) < rl.window { + valid = append(valid, t) + } + } + if len(valid) == 0 { + delete(rl.requests, key) + } else { + rl.requests[key] = valid + } + } +} + +func (rl *RateLimiter) Allow(key string) bool { + rl.mu.Lock() + defer rl.mu.Unlock() + now := time.Now() + windowStart := now.Add(-rl.window) + + var valid []time.Time + for _, t := range rl.requests[key] { + if t.After(windowStart) { + valid = append(valid, t) + } + } + + if len(valid) >= rl.limit { + rl.requests[key] = valid + return false + } + + rl.requests[key] = append(valid, now) + return true +} + +// RateLimit middleware - limits requests per IP +func RateLimit(limit int, window time.Duration) func(http.Handler) http.Handler { + limiter := NewRateLimiter(limit, window) + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Get client IP + ip := chi.URLParam(r, "X-Real-IP") + if ip == "" { + ip = r.RemoteAddr + } + + if !limiter.Allow(ip) { + http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests) + return + } + + next.ServeHTTP(w, r) + }) + } +}