package api import ( "fmt" "strconv" "sync" "time" "github.com/gofiber/fiber/v2" ) // ═══════════════════════════════════════════════════════════ // 🚦 RATE LIMITING MIDDLEWARE // ═══════════════════════════════════════════════════════════ // RateLimitConfig configuração do rate limiter type RateLimitConfig struct { // Requests máximos por janela Max int // Janela de tempo Window time.Duration // Função para extrair identificador (default: IP) KeyGenerator func(*fiber.Ctx) string // Função para resposta quando limitado LimitReached func(*fiber.Ctx) error // Pular rate limit para certos paths SkipPaths []string // Headers customizados Headers RateLimitHeaders } // RateLimitHeaders headers do rate limit type RateLimitHeaders struct { Limit string // X-RateLimit-Limit Remaining string // X-RateLimit-Remaining Reset string // X-RateLimit-Reset RetryAfter string // Retry-After } // DefaultRateLimitConfig configuração padrão func DefaultRateLimitConfig() RateLimitConfig { return RateLimitConfig{ Max: 100, Window: time.Minute, KeyGenerator: func(c *fiber.Ctx) string { return c.IP() }, LimitReached: func(c *fiber.Ctx) error { return c.Status(429).JSON(fiber.Map{ "error": "Too Many Requests", "message": "Rate limit exceeded. Please slow down.", "retry_after": 60, }) }, Headers: RateLimitHeaders{ Limit: "X-RateLimit-Limit", Remaining: "X-RateLimit-Remaining", Reset: "X-RateLimit-Reset", RetryAfter: "Retry-After", }, } } // rateLimitStore armazena contagem de requests type rateLimitStore struct { sync.RWMutex entries map[string]*rateLimitEntry } type rateLimitEntry struct { count int expiresAt time.Time } var store = &rateLimitStore{ entries: make(map[string]*rateLimitEntry), } // Limpar entries expirados periodicamente func init() { go func() { ticker := time.NewTicker(time.Minute) for range ticker.C { store.cleanup() } }() } func (s *rateLimitStore) cleanup() { s.Lock() defer s.Unlock() now := time.Now() for key, entry := range s.entries { if now.After(entry.expiresAt) { delete(s.entries, key) } } } // RateLimit middleware de rate limiting func RateLimit(config ...RateLimitConfig) fiber.Handler { cfg := DefaultRateLimitConfig() if len(config) > 0 { cfg = config[0] } return func(c *fiber.Ctx) error { // Verificar skip paths path := c.Path() for _, skip := range cfg.SkipPaths { if path == skip { return c.Next() } } // Gerar key key := cfg.KeyGenerator(c) // Verificar/atualizar contagem store.Lock() now := time.Now() entry, exists := store.entries[key] if !exists || now.After(entry.expiresAt) { // Nova janela store.entries[key] = &rateLimitEntry{ count: 1, expiresAt: now.Add(cfg.Window), } store.Unlock() // Headers c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max)) c.Set(cfg.Headers.Remaining, strconv.Itoa(cfg.Max-1)) c.Set(cfg.Headers.Reset, strconv.FormatInt(now.Add(cfg.Window).Unix(), 10)) return c.Next() } entry.count++ remaining := cfg.Max - entry.count resetTime := entry.expiresAt.Unix() store.Unlock() // Headers c.Set(cfg.Headers.Limit, strconv.Itoa(cfg.Max)) c.Set(cfg.Headers.Remaining, strconv.Itoa(max(0, remaining))) c.Set(cfg.Headers.Reset, strconv.FormatInt(resetTime, 10)) // Verificar se excedeu if remaining < 0 { retryAfter := int(time.Until(entry.expiresAt).Seconds()) c.Set(cfg.Headers.RetryAfter, strconv.Itoa(max(1, retryAfter))) return cfg.LimitReached(c) } return c.Next() } } // ═══════════════════════════════════════════════════════════ // 🎯 RATE LIMIT PRESETS // ═══════════════════════════════════════════════════════════ // RateLimitAuth rate limit para endpoints de autenticação (mais restritivo) func RateLimitAuth() fiber.Handler { return RateLimit(RateLimitConfig{ Max: 5, Window: time.Minute, KeyGenerator: func(c *fiber.Ctx) string { // Combinar IP + email para prevenir ataques distribuídos email := c.FormValue("email") if email == "" { email = c.Query("email") } return fmt.Sprintf("auth:%s:%s", c.IP(), email) }, LimitReached: func(c *fiber.Ctx) error { return c.Status(429).JSON(fiber.Map{ "error": "Too Many Login Attempts", "message": "You have exceeded the maximum number of login attempts. Please wait before trying again.", "retry_after": 60, }) }, }) } // RateLimitAPI rate limit para API geral func RateLimitAPI() fiber.Handler { return RateLimit(RateLimitConfig{ Max: 100, Window: time.Minute, KeyGenerator: func(c *fiber.Ctx) string { // Usar API key ou IP if apiKey := c.Locals("api_key_id"); apiKey != nil { return fmt.Sprintf("api:%v", apiKey) } if userID := c.Locals("user_id"); userID != nil { return fmt.Sprintf("user:%v", userID) } return fmt.Sprintf("ip:%s", c.IP()) }, }) } // RateLimitIngest rate limit para ingestão de dados (mais permissivo) func RateLimitIngest() fiber.Handler { return RateLimit(RateLimitConfig{ Max: 1000, Window: time.Minute, KeyGenerator: func(c *fiber.Ctx) string { if orgID := c.Locals("org_id"); orgID != nil { return fmt.Sprintf("ingest:%v", orgID) } return fmt.Sprintf("ingest:ip:%s", c.IP()) }, LimitReached: func(c *fiber.Ctx) error { return c.Status(429).JSON(fiber.Map{ "error": "Ingest Rate Limit Exceeded", "message": "Your organization has exceeded the data ingestion rate limit.", "retry_after": 10, }) }, }) } // RateLimitExport rate limit para exportação de dados func RateLimitExport() fiber.Handler { return RateLimit(RateLimitConfig{ Max: 10, Window: time.Hour, KeyGenerator: func(c *fiber.Ctx) string { if userID := c.Locals("user_id"); userID != nil { return fmt.Sprintf("export:%v", userID) } return fmt.Sprintf("export:ip:%s", c.IP()) }, LimitReached: func(c *fiber.Ctx) error { return c.Status(429).JSON(fiber.Map{ "error": "Export Rate Limit Exceeded", "message": "You have exceeded the maximum number of exports per hour.", "retry_after": 3600, }) }, }) } // ═══════════════════════════════════════════════════════════ // 🔧 HELPERS // ═══════════════════════════════════════════════════════════ func max(a, b int) int { if a > b { return a } return b }