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:
michael 2025-08-29 20:01:47 +02:00
parent ecd4f68595
commit 5c17c86fd4
4 changed files with 128 additions and 49 deletions

View file

@ -4,8 +4,20 @@
# Server configuration # Server configuration
[server] [server]
host = 127.0.0.1 host = 127.0.0.1
port = 8080 port = 7811
log_level = info 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) # Default SMTP settings (used when API keys don't have custom SMTP)
[smtp_default] [smtp_default]

View file

@ -7,6 +7,45 @@ local ConfigParser = require("src.config_parser")
-- Load configuration from furt.conf -- Load configuration from furt.conf
local config = ConfigParser.load_config() 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 -- Add legacy compatibility and runtime enhancements
local server_config = { local server_config = {
-- HTTP Server settings (from [server] section) -- HTTP Server settings (from [server] section)
@ -16,33 +55,21 @@ local server_config = {
-- Timeouts and limits -- Timeouts and limits
client_timeout = config.server.client_timeout or 10, client_timeout = config.server.client_timeout or 10,
-- CORS Configuration -- CORS Configuration (prioritize config file over environment)
cors = { cors = {
allowed_origins = (function() allowed_origins = get_cors_origins()
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)()
}, },
-- Logging -- Logging
log_level = config.server.log_level or "info", log_level = config.server.log_level or "info",
log_requests = config.server.log_requests or true, 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 (converted from nginx-style to old format for backward compatibility)
api_keys = config.api_keys, api_keys = config.api_keys,
@ -62,8 +89,18 @@ local server_config = {
print("Furt Multi-Tenant Configuration Loaded:") print("Furt Multi-Tenant Configuration Loaded:")
print(" Server: " .. server_config.host .. ":" .. server_config.port) print(" Server: " .. server_config.host .. ":" .. server_config.port)
print(" Log Level: " .. server_config.log_level) 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(" Default SMTP: " .. (config.smtp_default.host or "not configured"))
-- Print API key information
local api_key_count = 0 local api_key_count = 0
for key_name, key_config in pairs(config.api_keys) do for key_name, key_config in pairs(config.api_keys) do
api_key_count = api_key_count + 1 api_key_count = api_key_count + 1

View file

@ -1,4 +1,4 @@
-- furt-lua/src/main.lua -- src/main.lua
-- Pure Lua HTTP-Server for Furt API-Gateway -- Pure Lua HTTP-Server for Furt API-Gateway
-- Dragons@Work Digital Sovereignty Project -- Dragons@Work Digital Sovereignty Project
@ -256,8 +256,17 @@ function FurtServer:start()
print("Content-Hash: " .. (version_info.content_hash or "unknown")) print("Content-Hash: " .. (version_info.content_hash or "unknown"))
print("VCS: " .. (version_info.vcs_info and version_info.vcs_info.hash or "none")) print("VCS: " .. (version_info.vcs_info and version_info.vcs_info.hash or "none"))
print("API-Key authentication: ENABLED") 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") print("Press Ctrl+C to stop")
while true do 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, smtp_configured = config.smtp_default and config.smtp_default.host ~= nil,
auth_enabled = true, auth_enabled = true,
rate_limiting = true, rate_limiting = true,
rate_limits = config.security and config.security.rate_limits,
merkwerk_integrated = version_info.source == "merkwerk" merkwerk_integrated = version_info.source == "merkwerk"
} }
} }
return server:create_response(200, response_data, nil, nil, request) return server:create_response(200, response_data, nil, nil, request)
end) end)
-- Test endpoint for development (disable in production) -- Test endpoint for development (configurable via furt.conf)
if os.getenv("ENABLE_TEST_ENDPOINT") == "true" then if config.security and config.security.enable_test_endpoint then
server:add_route("POST", "/test", function(request, server) server:add_route("POST", "/test", function(request, server)
local response_data = { local response_data = {
message = "Test endpoint working", message = "Test endpoint working",
received_data = request.body, received_data = request.body,
headers_count = 0, headers_count = 0,
warning = "This is a development endpoint" warning = "This is a development endpoint (enabled via config)"
} }
-- Count headers -- Count headers
@ -311,7 +321,7 @@ if os.getenv("ENABLE_TEST_ENDPOINT") == "true" then
return server:create_response(200, response_data, nil, nil, request) return server:create_response(200, response_data, nil, nil, request)
end) end)
print("[WARN] Test endpoint enabled (development mode)") print("[WARN] Test endpoint enabled via configuration")
end end
-- Protected routes (require authentication) -- Protected routes (require authentication)

View file

@ -1,4 +1,4 @@
-- furt-lua/src/rate_limiter.lua -- src/rate_limiter.lua
-- Rate limiting system for API requests -- Rate limiting system for API requests
-- Dragons@Work Digital Sovereignty Project -- Dragons@Work Digital Sovereignty Project
@ -6,8 +6,8 @@ local RateLimiter = {
requests = {}, -- {api_key = {timestamps}, ip = {timestamps}} requests = {}, -- {api_key = {timestamps}, ip = {timestamps}}
cleanup_interval = 300, -- Cleanup every 5 minutes cleanup_interval = 300, -- Cleanup every 5 minutes
last_cleanup = os.time(), last_cleanup = os.time(),
-- Default limits -- Default limits (configurable)
default_limits = { default_limits = {
api_key_max = 60, -- 60 requests per hour per API key api_key_max = 60, -- 60 requests per hour per API key
ip_max = 100, -- 100 requests per hour per IP 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 -- Cleanup old requests from memory
function RateLimiter:cleanup_old_requests() function RateLimiter:cleanup_old_requests()
local now = os.time() local now = os.time()
if now - self.last_cleanup < self.cleanup_interval then if now - self.last_cleanup < self.cleanup_interval then
return return
end end
local cutoff = now - self.default_limits.window local cutoff = now - self.default_limits.window
for key, timestamps in pairs(self.requests) do for key, timestamps in pairs(self.requests) do
local filtered = {} local filtered = {}
for _, timestamp in ipairs(timestamps) do for _, timestamp in ipairs(timestamps) do
@ -33,21 +53,21 @@ function RateLimiter:cleanup_old_requests()
end end
self.requests[key] = filtered self.requests[key] = filtered
end end
self.last_cleanup = now self.last_cleanup = now
end end
-- Check if request is within rate limit -- Check if request is within rate limit
function RateLimiter:check_rate_limit(key, max_requests, window_seconds) function RateLimiter:check_rate_limit(key, max_requests, window_seconds)
self:cleanup_old_requests() self:cleanup_old_requests()
local now = os.time() local now = os.time()
local cutoff = now - (window_seconds or self.default_limits.window) local cutoff = now - (window_seconds or self.default_limits.window)
if not self.requests[key] then if not self.requests[key] then
self.requests[key] = {} self.requests[key] = {}
end end
-- Count requests in time window -- Count requests in time window
local count = 0 local count = 0
for _, timestamp in ipairs(self.requests[key]) do 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 count = count + 1
end end
end end
-- Check if limit exceeded -- Check if limit exceeded
if count >= max_requests then if count >= max_requests then
return false, count, max_requests - count return false, count, max_requests - count
end end
-- Record this request -- Record this request
table.insert(self.requests[key], now) table.insert(self.requests[key], now)
return true, count + 1, max_requests - (count + 1) return true, count + 1, max_requests - (count + 1)
end end
@ -71,11 +91,11 @@ end
function RateLimiter:check_api_and_ip_limits(api_key, client_ip) function RateLimiter:check_api_and_ip_limits(api_key, client_ip)
-- Check API key rate limit -- Check API key rate limit
local api_key_allowed, api_count, api_remaining = self:check_rate_limit( local api_key_allowed, api_count, api_remaining = self:check_rate_limit(
"api_key:" .. api_key, "api_key:" .. api_key,
self.default_limits.api_key_max, self.default_limits.api_key_max,
self.default_limits.window self.default_limits.window
) )
if not api_key_allowed then if not api_key_allowed then
return false, "API key rate limit exceeded", { return false, "API key rate limit exceeded", {
type = "api_key", type = "api_key",
@ -84,14 +104,14 @@ function RateLimiter:check_api_and_ip_limits(api_key, client_ip)
remaining = api_remaining remaining = api_remaining
} }
end end
-- Check IP rate limit -- Check IP rate limit
local ip_allowed, ip_count, ip_remaining = self:check_rate_limit( local ip_allowed, ip_count, ip_remaining = self:check_rate_limit(
"ip:" .. client_ip, "ip:" .. client_ip,
self.default_limits.ip_max, self.default_limits.ip_max,
self.default_limits.window self.default_limits.window
) )
if not ip_allowed then if not ip_allowed then
return false, "IP rate limit exceeded", { return false, "IP rate limit exceeded", {
type = "ip", type = "ip",
@ -100,7 +120,7 @@ function RateLimiter:check_api_and_ip_limits(api_key, client_ip)
remaining = ip_remaining remaining = ip_remaining
} }
end end
-- Both limits OK -- Both limits OK
return true, "OK", { return true, "OK", {
api_key = { 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 if not limit_info or not limit_info.api_key then
return {} return {}
end end
return { return {
["X-RateLimit-Remaining"] = tostring(limit_info.api_key.remaining or 0), ["X-RateLimit-Remaining"] = tostring(limit_info.api_key.remaining or 0),
["X-RateLimit-Limit"] = tostring(self.default_limits.api_key_max), ["X-RateLimit-Limit"] = tostring(self.default_limits.api_key_max),