Files
CalendarApi/internal/middleware/ratelimit.go
Michilis 75105b8b46 Add OpenAPI docs, frontend, migrations, and API updates
- OpenAPI: add missing endpoints (add-from-url, subscriptions, public availability)
- OpenAPI: CalendarSubscription schema, Subscriptions tag
- Frontend app
- Migrations: count_for_availability, subscriptions_sync, user_preferences, calendar_settings
- Config, rate limit, auth, calendar, booking, ICS, availability, user service updates

Made-with: Cursor
2026-03-02 14:07:55 +00:00

93 lines
1.7 KiB
Go

package middleware
import (
"net/http"
"strings"
"sync"
"time"
"github.com/calendarapi/internal/models"
"github.com/calendarapi/internal/utils"
)
type visitor struct {
tokens float64
lastSeen time.Time
}
type RateLimiter struct {
mu sync.Mutex
visitors map[string]*visitor
rate float64
burst float64
}
func NewRateLimiter(ratePerSecond float64, burst int) *RateLimiter {
rl := &RateLimiter{
visitors: make(map[string]*visitor),
rate: ratePerSecond,
burst: float64(burst),
}
go rl.cleanup()
return rl
}
func (rl *RateLimiter) Limit(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
if fwd := r.Header.Get("X-Forwarded-For"); fwd != "" {
// Use leftmost (client) IP when behind proxies
if idx := strings.Index(fwd, ","); idx > 0 {
ip = strings.TrimSpace(fwd[:idx])
} else {
ip = strings.TrimSpace(fwd)
}
}
if !rl.allow(ip) {
utils.WriteError(w, models.ErrRateLimited)
return
}
next.ServeHTTP(w, r)
})
}
func (rl *RateLimiter) allow(key string) bool {
rl.mu.Lock()
defer rl.mu.Unlock()
v, exists := rl.visitors[key]
now := time.Now()
if !exists {
rl.visitors[key] = &visitor{tokens: rl.burst - 1, lastSeen: now}
return true
}
elapsed := now.Sub(v.lastSeen).Seconds()
v.tokens += elapsed * rl.rate
if v.tokens > rl.burst {
v.tokens = rl.burst
}
v.lastSeen = now
if v.tokens < 1 {
return false
}
v.tokens--
return true
}
func (rl *RateLimiter) cleanup() {
for {
time.Sleep(5 * time.Minute)
rl.mu.Lock()
for key, v := range rl.visitors {
if time.Since(v.lastSeen) > 10*time.Minute {
delete(rl.visitors, key)
}
}
rl.mu.Unlock()
}
}