Pulse/internal/api/ratelimit.go
2025-10-11 23:29:47 +00:00

104 lines
1.9 KiB
Go

package api
import (
"net/http"
"sync"
"time"
)
type RateLimiter struct {
attempts map[string][]time.Time
mu sync.RWMutex
limit int
window time.Duration
}
func NewRateLimiter(limit int, window time.Duration) *RateLimiter {
rl := &RateLimiter{
attempts: make(map[string][]time.Time),
limit: limit,
window: window,
}
// Clean up old entries periodically
go func() {
ticker := time.NewTicker(1 * time.Minute)
defer ticker.Stop()
for range ticker.C {
rl.cleanup()
}
}()
return rl
}
func (rl *RateLimiter) Allow(ip string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
cutoff := now.Add(-rl.window)
// Get attempts for this IP
attempts := rl.attempts[ip]
// Filter out old attempts
var validAttempts []time.Time
for _, attempt := range attempts {
if attempt.After(cutoff) {
validAttempts = append(validAttempts, attempt)
}
}
// Check if under limit
if len(validAttempts) >= rl.limit {
rl.attempts[ip] = validAttempts
return false
}
// Add new attempt
validAttempts = append(validAttempts, now)
rl.attempts[ip] = validAttempts
return true
}
func (rl *RateLimiter) cleanup() {
rl.mu.Lock()
defer rl.mu.Unlock()
cutoff := time.Now().Add(-rl.window)
for ip, attempts := range rl.attempts {
var validAttempts []time.Time
for _, attempt := range attempts {
if attempt.After(cutoff) {
validAttempts = append(validAttempts, attempt)
}
}
if len(validAttempts) == 0 {
delete(rl.attempts, ip)
} else {
rl.attempts[ip] = validAttempts
}
}
}
// Middleware for rate limiting
func (rl *RateLimiter) Middleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
if forwarded := r.Header.Get("X-Forwarded-For"); forwarded != "" {
ip = forwarded
}
if !rl.Allow(ip) {
http.Error(w, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests)
return
}
next(w, r)
}
}