package middleware import ( "net/http" "sync" "time" "music-server/internal/logging" "github.com/labstack/echo/v5" "go.uber.org/zap" ) // RateLimiter implements a simple IP-based rate limiter type RateLimiter struct { visits map[string]*Visit mu sync.Mutex limit int // requests per window window time.Duration // time window } type Visit struct { count int firstSeen time.Time } // NewRateLimiter creates a new rate limiter func NewRateLimiter(limit int, window time.Duration) *RateLimiter { rl := &RateLimiter{ visits: make(map[string]*Visit), limit: limit, window: window, } // Start cleanup goroutine go rl.cleanup() return rl } // cleanup periodically removes old entries func (rl *RateLimiter) cleanup() { for { time.Sleep(rl.window) rl.mu.Lock() now := time.Now() for ip, visit := range rl.visits { if now.Sub(visit.firstSeen) > rl.window { delete(rl.visits, ip) } } rl.mu.Unlock() } } // RateLimitMiddleware returns an Echo middleware for rate limiting func RateLimitMiddleware(rl *RateLimiter) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c *echo.Context) error { ip := c.RealIP() rl.mu.Lock() defer rl.mu.Unlock() visit, exists := rl.visits[ip] now := time.Now() if !exists { // First visit from this IP rl.visits[ip] = &Visit{ count: 1, firstSeen: now, } return next(c) } // Check if window has expired if now.Sub(visit.firstSeen) > rl.window { // Reset for new window visit.count = 1 visit.firstSeen = now return next(c) } // Check if limit exceeded if visit.count >= rl.limit { logging.GetLogger().Warn("Rate limit exceeded", zap.String("ip", ip), zap.String("path", c.Request().URL.Path)) return c.JSON(http.StatusTooManyRequests, map[string]string{ "error": "Rate limit exceeded. Please try again later.", }) } // Increment count visit.count++ return next(c) } } }