diff --git a/config/furt.conf.example b/config/furt.conf.example index ea49675..28ffc8a 100644 --- a/config/furt.conf.example +++ b/config/furt.conf.example @@ -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] diff --git a/config/server.lua b/config/server.lua index f2a05a7..7c4ccfd 100644 --- a/config/server.lua +++ b/config/server.lua @@ -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 diff --git a/src/main.lua b/src/main.lua index 7e0268c..8ee7281 100644 --- a/src/main.lua +++ b/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) diff --git a/src/rate_limiter.lua b/src/rate_limiter.lua index 0d689c9..07b7a49 100644 --- a/src/rate_limiter.lua +++ b/src/rate_limiter.lua @@ -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),