refactor: convert rate limiter to struct-based implementation

This commit is contained in:
2026-05-06 23:18:35 +02:00
parent 7512aac53e
commit 03d9f4bd0d

View File

@@ -13,33 +13,30 @@ type visitor struct {
lastSeen time.Time lastSeen time.Time
} }
var ( type Config struct {
visitors = make(map[string]*visitor) MaxAttempts int
Window time.Duration
}
type Limiter struct {
mu sync.Mutex mu sync.Mutex
quit = make(chan struct{}) visitors map[string]*visitor
) config Config
func init() {
go cleanupVisitors()
} }
func StopCleanup() { func NewLimiter(cfg Config) *Limiter {
close(quit) return &Limiter{
visitors: make(map[string]*visitor),
config: cfg,
}
} }
func cleanupVisitors() { func (l *Limiter) Cleanup(now time.Time) {
for { l.mu.Lock()
select { defer l.mu.Unlock()
case <-quit: for ip, v := range l.visitors {
return if now.Sub(v.lastSeen) > l.config.Window*3 {
case <-time.After(time.Minute): delete(l.visitors, ip)
mu.Lock()
for ip, v := range visitors {
if time.Since(v.lastSeen) > 3*time.Minute {
delete(visitors, ip)
}
}
mu.Unlock()
} }
} }
} }
@@ -59,26 +56,19 @@ func getIP(r *http.Request) string {
return ip return ip
} }
func RateLimitAuth(next http.Handler) http.Handler { func (l *Limiter) Middleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := getIP(r) if !l.allow(getIP(r)) {
http.Error(w, "Too many requests. Please try again later.", http.StatusTooManyRequests)
mu.Lock() return
v, exists := visitors[ip]
if !exists {
visitors[ip] = &visitor{1, time.Now()}
} else {
// Reset attempts if it's been more than a minute
if time.Since(v.lastSeen) > time.Minute {
v.attempts = 0
}
v.attempts++
v.lastSeen = time.Now()
} }
next.ServeHTTP(w, r)
})
}
// If 5 or more attempts within a minute, block func (l *Limiter) AuthMiddleware(next http.Handler) http.Handler {
if exists && v.attempts >= 5 { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
mu.Unlock() if !l.allow(getIP(r)) {
if strings.HasPrefix(r.URL.Path, "/") { if strings.HasPrefix(r.URL.Path, "/") {
http.Redirect(w, r, fmt.Sprintf("%s?error=rate_limited", r.URL.Path), http.StatusFound) http.Redirect(w, r, fmt.Sprintf("%s?error=rate_limited", r.URL.Path), http.StatusFound)
return return
@@ -86,8 +76,27 @@ func RateLimitAuth(next http.Handler) http.Handler {
http.Error(w, "Too many requests. Please try again later.", http.StatusTooManyRequests) http.Error(w, "Too many requests. Please try again later.", http.StatusTooManyRequests)
return return
} }
mu.Unlock()
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
func (l *Limiter) allow(ip string) bool {
l.mu.Lock()
defer l.mu.Unlock()
v, exists := l.visitors[ip]
if !exists {
l.visitors[ip] = &visitor{1, time.Now()}
return true
}
if time.Since(v.lastSeen) > l.config.Window {
v.attempts = 1
v.lastSeen = time.Now()
return true
}
v.attempts++
v.lastSeen = time.Now()
return v.attempts <= l.config.MaxAttempts
}