-- src/rate_limiter.lua -- Rate limiting system for API requests -- Dragons@Work Digital Sovereignty Project local RateLimiter = { requests = {}, -- {api_key = {timestamps}, ip = {timestamps}} cleanup_interval = 300, -- Cleanup every 5 minutes last_cleanup = os.time(), -- Default limits (configurable) default_limits = { api_key_max = 60, -- 60 requests per hour per API key ip_max = 100, -- 100 requests per hour per IP window = 3600 -- 1 hour window } } -- Configure rate limits from config function RateLimiter:configure(limits) if limits then if limits.api_key_max then self.default_limits.api_key_max = limits.api_key_max end if limits.ip_max then self.default_limits.ip_max = limits.ip_max end if limits.window then self.default_limits.window = limits.window end print("Rate limiting configured:") print(" API Key limit: " .. self.default_limits.api_key_max .. " req/hour") print(" IP limit: " .. self.default_limits.ip_max .. " req/hour") print(" Window: " .. self.default_limits.window .. " seconds") end end -- Cleanup old requests from memory function RateLimiter:cleanup_old_requests() local now = os.time() if now - self.last_cleanup < self.cleanup_interval then return end local cutoff = now - self.default_limits.window for key, timestamps in pairs(self.requests) do local filtered = {} for _, timestamp in ipairs(timestamps) do if timestamp > cutoff then table.insert(filtered, timestamp) end end self.requests[key] = filtered end self.last_cleanup = now end -- Check if request is within rate limit function RateLimiter:check_rate_limit(key, max_requests, window_seconds) self:cleanup_old_requests() local now = os.time() local cutoff = now - (window_seconds or self.default_limits.window) if not self.requests[key] then self.requests[key] = {} end -- Count requests in time window local count = 0 for _, timestamp in ipairs(self.requests[key]) do if timestamp > cutoff then count = count + 1 end end -- Check if limit exceeded if count >= max_requests then return false, count, max_requests - count end -- Record this request table.insert(self.requests[key], now) return true, count + 1, max_requests - (count + 1) end -- Check rate limits for API key and IP function RateLimiter:check_api_and_ip_limits(api_key, client_ip) -- Check API key rate limit local api_key_allowed, api_count, api_remaining = self:check_rate_limit( "api_key:" .. api_key, self.default_limits.api_key_max, self.default_limits.window ) if not api_key_allowed then return false, "API key rate limit exceeded", { type = "api_key", current = api_count, limit = self.default_limits.api_key_max, remaining = api_remaining } end -- Check IP rate limit local ip_allowed, ip_count, ip_remaining = self:check_rate_limit( "ip:" .. client_ip, self.default_limits.ip_max, self.default_limits.window ) if not ip_allowed then return false, "IP rate limit exceeded", { type = "ip", current = ip_count, limit = self.default_limits.ip_max, remaining = ip_remaining } end -- Both limits OK return true, "OK", { api_key = { current = api_count, limit = self.default_limits.api_key_max, remaining = api_remaining }, ip = { current = ip_count, limit = self.default_limits.ip_max, remaining = ip_remaining } } end -- Get rate limit headers for HTTP response function RateLimiter:get_rate_limit_headers(limit_info) if not limit_info or not limit_info.api_key then return {} end return { ["X-RateLimit-Remaining"] = tostring(limit_info.api_key.remaining or 0), ["X-RateLimit-Limit"] = tostring(self.default_limits.api_key_max), ["X-RateLimit-Window"] = tostring(self.default_limits.window) } end return RateLimiter