134 lines
3.7 KiB
Lua
134 lines
3.7 KiB
Lua
|
|
-- furt-lua/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
|
||
|
|
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
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
-- 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
|
||
|
|
|