docent/server/ratelimit.go

81 lines
1.5 KiB
Go
Raw Permalink Normal View History

2026-05-05 09:33:51 -05:00
package main
import (
"net"
"net/http"
"strings"
"sync"
"time"
)
type rateLimiter struct {
mu sync.Mutex
visitors map[string]*visitor
rate int
window time.Duration
}
type visitor struct {
count int
reset time.Time
}
func newRateLimiter(rate int, window time.Duration) *rateLimiter {
rl := &rateLimiter{
visitors: make(map[string]*visitor),
rate: rate,
window: window,
}
go rl.cleanup()
return rl
}
func (rl *rateLimiter) allow(key string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
v, ok := rl.visitors[key]
if !ok || now.After(v.reset) {
rl.visitors[key] = &visitor{count: 1, reset: now.Add(rl.window)}
return true
}
if v.count >= rl.rate {
return false
}
v.count++
return true
}
func (rl *rateLimiter) cleanup() {
for {
time.Sleep(5 * time.Minute)
rl.mu.Lock()
now := time.Now()
for k, v := range rl.visitors {
if now.After(v.reset) {
delete(rl.visitors, k)
}
}
rl.mu.Unlock()
}
}
// clientIP extracts the client address from X-Forwarded-For (set by nginx),
// falling back to X-Real-IP and finally RemoteAddr.
func clientIP(r *http.Request) string {
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
if comma := strings.Index(xff, ","); comma > 0 {
return strings.TrimSpace(xff[:comma])
}
return strings.TrimSpace(xff)
}
if rip := r.Header.Get("X-Real-IP"); rip != "" {
return rip
}
host, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
return r.RemoteAddr
}
return host
}