feat(config): integrate rate limiting and CORS configuration from furt.conf
- Add RateLimiter:configure() function to accept config-based limits - Integrate security section parameters (rate_limit_api_key_max, ip_max, window) - Add CORS configuration from config file with environment fallback - Replace hardcoded rate limiting defaults with configurable values - Add test endpoint control via config.security.enable_test_endpoint - Update startup logging to show actual configured rate limits - Add configuration validation and detailed startup information Rate limiting now uses values from [security] section instead of hardcoded defaults. CORS origins prioritize config file over environment variables. Related to DAW/furt#89
This commit is contained in:
parent
ecd4f68595
commit
5c17c86fd4
4 changed files with 128 additions and 49 deletions
|
|
@ -4,8 +4,20 @@
|
|||
# Server configuration
|
||||
[server]
|
||||
host = 127.0.0.1
|
||||
port = 8080
|
||||
port = 7811
|
||||
log_level = info
|
||||
log_requests = true
|
||||
client_timeout = 10
|
||||
|
||||
# CORS configuration
|
||||
cors_allowed_origins = http://localhost:1313,http://127.0.0.1:1313,https://dragons-at-work.de,https://www.dragons-at-work.de
|
||||
|
||||
# Security settings
|
||||
[security]
|
||||
rate_limit_api_key_max = 60
|
||||
rate_limit_ip_max = 100
|
||||
rate_limit_window = 3600
|
||||
enable_test_endpoint = false
|
||||
|
||||
# Default SMTP settings (used when API keys don't have custom SMTP)
|
||||
[smtp_default]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,45 @@ local ConfigParser = require("src.config_parser")
|
|||
-- Load configuration from furt.conf
|
||||
local config = ConfigParser.load_config()
|
||||
|
||||
-- Configure rate limiting from config
|
||||
local RateLimiter = require("src.rate_limiter")
|
||||
local rate_limits = {
|
||||
api_key_max = config.security and config.security.rate_limit_api_key_max or 60,
|
||||
ip_max = config.security and config.security.rate_limit_ip_max or 100,
|
||||
window = config.security and config.security.rate_limit_window or 3600
|
||||
}
|
||||
RateLimiter:configure(rate_limits)
|
||||
|
||||
-- Parse CORS origins from config or environment
|
||||
local function get_cors_origins()
|
||||
-- 1. Try config file first
|
||||
if config.server.cors_allowed_origins then
|
||||
local origins = {}
|
||||
for origin in config.server.cors_allowed_origins:gmatch("([^,]+)") do
|
||||
table.insert(origins, origin:match("^%s*(.-)%s*$"))
|
||||
end
|
||||
return origins
|
||||
end
|
||||
|
||||
-- 2. Try environment variable
|
||||
local env_origins = os.getenv("CORS_ALLOWED_ORIGINS")
|
||||
if env_origins then
|
||||
local origins = {}
|
||||
for origin in env_origins:gmatch("([^,]+)") do
|
||||
table.insert(origins, origin:match("^%s*(.-)%s*$"))
|
||||
end
|
||||
return origins
|
||||
end
|
||||
|
||||
-- 3. Development defaults
|
||||
return {
|
||||
"http://localhost:1313", -- Hugo dev server
|
||||
"http://127.0.0.1:1313", -- Hugo dev server alternative
|
||||
"http://localhost:3000", -- Common dev port
|
||||
"http://127.0.0.1:3000" -- Common dev port alternative
|
||||
}
|
||||
end
|
||||
|
||||
-- Add legacy compatibility and runtime enhancements
|
||||
local server_config = {
|
||||
-- HTTP Server settings (from [server] section)
|
||||
|
|
@ -16,33 +55,21 @@ local server_config = {
|
|||
-- Timeouts and limits
|
||||
client_timeout = config.server.client_timeout or 10,
|
||||
|
||||
-- CORS Configuration
|
||||
-- CORS Configuration (prioritize config file over environment)
|
||||
cors = {
|
||||
allowed_origins = (function()
|
||||
local env_origins = os.getenv("CORS_ALLOWED_ORIGINS")
|
||||
if env_origins then
|
||||
-- Parse comma-separated list from environment
|
||||
local origins = {}
|
||||
for origin in env_origins:gmatch("([^,]+)") do
|
||||
table.insert(origins, origin:match("^%s*(.-)%s*$"))
|
||||
end
|
||||
return origins
|
||||
else
|
||||
-- Default development origins
|
||||
return {
|
||||
"http://localhost:1313", -- Hugo dev server
|
||||
"http://127.0.0.1:1313", -- Hugo dev server alternative
|
||||
"http://localhost:3000", -- Common dev port
|
||||
"http://127.0.0.1:3000" -- Common dev port alternative
|
||||
}
|
||||
end
|
||||
end)()
|
||||
allowed_origins = get_cors_origins()
|
||||
},
|
||||
|
||||
-- Logging
|
||||
log_level = config.server.log_level or "info",
|
||||
log_requests = config.server.log_requests or true,
|
||||
|
||||
-- Security settings
|
||||
security = {
|
||||
enable_test_endpoint = config.security and config.security.enable_test_endpoint or false,
|
||||
rate_limits = rate_limits
|
||||
},
|
||||
|
||||
-- API Keys (converted from nginx-style to old format for backward compatibility)
|
||||
api_keys = config.api_keys,
|
||||
|
||||
|
|
@ -62,8 +89,18 @@ local server_config = {
|
|||
print("Furt Multi-Tenant Configuration Loaded:")
|
||||
print(" Server: " .. server_config.host .. ":" .. server_config.port)
|
||||
print(" Log Level: " .. server_config.log_level)
|
||||
|
||||
-- Print CORS configuration
|
||||
print(" CORS Origins:")
|
||||
for i, origin in ipairs(server_config.cors.allowed_origins) do
|
||||
print(" " .. i .. ": " .. origin)
|
||||
end
|
||||
|
||||
-- Print security configuration
|
||||
print(" Test Endpoint: " .. (server_config.security.enable_test_endpoint and "enabled" or "disabled"))
|
||||
print(" Default SMTP: " .. (config.smtp_default.host or "not configured"))
|
||||
|
||||
-- Print API key information
|
||||
local api_key_count = 0
|
||||
for key_name, key_config in pairs(config.api_keys) do
|
||||
api_key_count = api_key_count + 1
|
||||
|
|
|
|||
24
src/main.lua
24
src/main.lua
|
|
@ -1,4 +1,4 @@
|
|||
-- furt-lua/src/main.lua
|
||||
-- src/main.lua
|
||||
-- Pure Lua HTTP-Server for Furt API-Gateway
|
||||
-- Dragons@Work Digital Sovereignty Project
|
||||
|
||||
|
|
@ -256,8 +256,17 @@ function FurtServer:start()
|
|||
print("Content-Hash: " .. (version_info.content_hash or "unknown"))
|
||||
print("VCS: " .. (version_info.vcs_info and version_info.vcs_info.hash or "none"))
|
||||
print("API-Key authentication: ENABLED")
|
||||
print("Rate limiting: ENABLED (60 req/hour per API key, 100 req/hour per IP)")
|
||||
print("CORS enabled for configured origins")
|
||||
|
||||
-- Show actual configured rate limits
|
||||
local rate_limits = config.security and config.security.rate_limits
|
||||
if rate_limits then
|
||||
print(string.format("Rate limiting: ENABLED (%d req/hour per API key, %d req/hour per IP)",
|
||||
rate_limits.api_key_max, rate_limits.ip_max))
|
||||
else
|
||||
print("Rate limiting: ENABLED (default values)")
|
||||
end
|
||||
|
||||
print("CORS enabled for " .. (#config.cors.allowed_origins) .. " configured origins")
|
||||
print("Press Ctrl+C to stop")
|
||||
|
||||
while true do
|
||||
|
|
@ -288,20 +297,21 @@ server:add_route("GET", "/health", function(request, server)
|
|||
smtp_configured = config.smtp_default and config.smtp_default.host ~= nil,
|
||||
auth_enabled = true,
|
||||
rate_limiting = true,
|
||||
rate_limits = config.security and config.security.rate_limits,
|
||||
merkwerk_integrated = version_info.source == "merkwerk"
|
||||
}
|
||||
}
|
||||
return server:create_response(200, response_data, nil, nil, request)
|
||||
end)
|
||||
|
||||
-- Test endpoint for development (disable in production)
|
||||
if os.getenv("ENABLE_TEST_ENDPOINT") == "true" then
|
||||
-- Test endpoint for development (configurable via furt.conf)
|
||||
if config.security and config.security.enable_test_endpoint then
|
||||
server:add_route("POST", "/test", function(request, server)
|
||||
local response_data = {
|
||||
message = "Test endpoint working",
|
||||
received_data = request.body,
|
||||
headers_count = 0,
|
||||
warning = "This is a development endpoint"
|
||||
warning = "This is a development endpoint (enabled via config)"
|
||||
}
|
||||
|
||||
-- Count headers
|
||||
|
|
@ -311,7 +321,7 @@ if os.getenv("ENABLE_TEST_ENDPOINT") == "true" then
|
|||
|
||||
return server:create_response(200, response_data, nil, nil, request)
|
||||
end)
|
||||
print("[WARN] Test endpoint enabled (development mode)")
|
||||
print("[WARN] Test endpoint enabled via configuration")
|
||||
end
|
||||
|
||||
-- Protected routes (require authentication)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- furt-lua/src/rate_limiter.lua
|
||||
-- src/rate_limiter.lua
|
||||
-- Rate limiting system for API requests
|
||||
-- Dragons@Work Digital Sovereignty Project
|
||||
|
||||
|
|
@ -6,8 +6,8 @@ local RateLimiter = {
|
|||
requests = {}, -- {api_key = {timestamps}, ip = {timestamps}}
|
||||
cleanup_interval = 300, -- Cleanup every 5 minutes
|
||||
last_cleanup = os.time(),
|
||||
|
||||
-- Default limits
|
||||
|
||||
-- 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
|
||||
|
|
@ -15,15 +15,35 @@ local RateLimiter = {
|
|||
}
|
||||
}
|
||||
|
||||
-- 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
|
||||
|
|
@ -33,21 +53,21 @@ function RateLimiter:cleanup_old_requests()
|
|||
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
|
||||
|
|
@ -55,15 +75,15 @@ function RateLimiter:check_rate_limit(key, max_requests, window_seconds)
|
|||
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
|
||||
|
||||
|
|
@ -71,11 +91,11 @@ end
|
|||
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,
|
||||
"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",
|
||||
|
|
@ -84,14 +104,14 @@ function RateLimiter:check_api_and_ip_limits(api_key, client_ip)
|
|||
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,
|
||||
"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",
|
||||
|
|
@ -100,7 +120,7 @@ function RateLimiter:check_api_and_ip_limits(api_key, client_ip)
|
|||
remaining = ip_remaining
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
-- Both limits OK
|
||||
return true, "OK", {
|
||||
api_key = {
|
||||
|
|
@ -121,7 +141,7 @@ 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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue