Spaces:
Runtime error
Runtime error
| package api | |
| import ( | |
| "net/http" | |
| "strconv" | |
| "sync" | |
| "time" | |
| ) | |
| // rateLimiter is a tiny per-key fixed-window counter. It is intentionally | |
| // simple (no external deps): each key gets `limit` requests per 60s window. | |
| type rateLimiter struct { | |
| mu sync.Mutex | |
| limit int | |
| window time.Duration | |
| hits map[string]*window | |
| } | |
| type window struct { | |
| count int | |
| reset time.Time | |
| } | |
| func newRateLimiter(rpm int) *rateLimiter { | |
| return &rateLimiter{limit: rpm, window: time.Minute, hits: make(map[string]*window)} | |
| } | |
| // allow reports whether a request for key is permitted and, if not, how long | |
| // until the window resets. | |
| func (rl *rateLimiter) allow(key string) (bool, time.Duration) { | |
| now := time.Now() | |
| rl.mu.Lock() | |
| defer rl.mu.Unlock() | |
| w := rl.hits[key] | |
| if w == nil || now.After(w.reset) { | |
| rl.hits[key] = &window{count: 1, reset: now.Add(rl.window)} | |
| // Opportunistically evict stale entries to bound memory. | |
| if len(rl.hits) > 4096 { | |
| for k, v := range rl.hits { | |
| if now.After(v.reset) { | |
| delete(rl.hits, k) | |
| } | |
| } | |
| } | |
| return true, 0 | |
| } | |
| if w.count >= rl.limit { | |
| return false, time.Until(w.reset) | |
| } | |
| w.count++ | |
| return true, 0 | |
| } | |
| // rateLimited reports whether a path should be rate limited: state-changing | |
| // methods on /v1, plus auth endpoints (to slow brute force). Read-only probes | |
| // and asset fetches are never limited. | |
| func rateLimited(r *http.Request) bool { | |
| if !pathHasPrefix(r.URL.Path, "/v1/") { | |
| return false | |
| } | |
| if r.Method != http.MethodGet && r.Method != http.MethodHead { | |
| return true | |
| } | |
| return false | |
| } | |
| func pathHasPrefix(p, prefix string) bool { | |
| return len(p) >= len(prefix) && p[:len(prefix)] == prefix | |
| } | |
| // withRateLimit wraps next with per-IP rate limiting on write/auth endpoints. | |
| func (s *Server) withRateLimit(next http.Handler) http.Handler { | |
| if s.cfg.RateLimitRPM <= 0 || s.limiter == nil { | |
| return next | |
| } | |
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| if rateLimited(r) { | |
| if ok, retry := s.limiter.allow(clientIP(r)); !ok { | |
| w.Header().Set("Retry-After", strconv.Itoa(int(retry.Seconds())+1)) | |
| writeError(w, http.StatusTooManyRequests, "rate limit exceeded — slow down and retry") | |
| return | |
| } | |
| } | |
| next.ServeHTTP(w, r) | |
| }) | |
| } | |