gohorsejobs/backend/internal/middleware/rate_limit.go
Tiago Yamamoto 7934afcf0d docs: complete project documentation overhaul
- Add comprehensive root README with badges, architecture diagram, and setup guide
- Update backend README with security middlewares and endpoint documentation
- Update frontend README with design system and page structure
- Update seeder-api README with generated data and credentials
- Add internal module READMEs (middleware, handlers, components)
- Document Clean Architecture layers and request flow
- Add environment variables reference table
2025-12-09 19:36:36 -03:00

117 lines
2.3 KiB
Go

package middleware
import (
"net/http"
"sync"
"time"
)
// RateLimiter implements a simple in-memory rate limiter
type RateLimiter struct {
visitors map[string]*visitor
mu sync.RWMutex
rate int // requests allowed
window time.Duration // time window
}
type visitor struct {
count int
lastReset time.Time
}
// NewRateLimiter creates a rate limiter with specified requests per window
func NewRateLimiter(rate int, window time.Duration) *RateLimiter {
rl := &RateLimiter{
visitors: make(map[string]*visitor),
rate: rate,
window: window,
}
// Cleanup old entries periodically
go rl.cleanup()
return rl
}
func (rl *RateLimiter) cleanup() {
for {
time.Sleep(rl.window)
rl.mu.Lock()
for ip, v := range rl.visitors {
if time.Since(v.lastReset) > rl.window*2 {
delete(rl.visitors, ip)
}
}
rl.mu.Unlock()
}
}
func (rl *RateLimiter) isAllowed(ip string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
v, exists := rl.visitors[ip]
now := time.Now()
if !exists {
rl.visitors[ip] = &visitor{count: 1, lastReset: now}
return true
}
// Reset window if needed
if now.Sub(v.lastReset) > rl.window {
v.count = 1
v.lastReset = now
return true
}
if v.count >= rl.rate {
return false
}
v.count++
return true
}
// getIP extracts client IP from request
func getIP(r *http.Request) string {
// Check X-Forwarded-For first (for proxied requests)
xff := r.Header.Get("X-Forwarded-For")
if xff != "" {
// Take the first IP in the chain
for i := 0; i < len(xff); i++ {
if xff[i] == ',' {
return xff[:i]
}
}
return xff
}
// Check X-Real-IP
xri := r.Header.Get("X-Real-IP")
if xri != "" {
return xri
}
// Fallback to RemoteAddr
return r.RemoteAddr
}
// RateLimitMiddleware returns a middleware that limits requests per IP
func RateLimitMiddleware(rate int, window time.Duration) func(http.Handler) http.Handler {
limiter := NewRateLimiter(rate, window)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := getIP(r)
if !limiter.isAllowed(ip) {
w.Header().Set("Retry-After", "60")
http.Error(w, "Rate limit exceeded. Please try again later.", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
}