-- src/http_server.lua -- HTTP Server Core for Furt API-Gateway -- Dragons@Work Digital Sovereignty Project local socket = require("socket") local found_cjson, cjson = pcall(require, 'cjson') if not found_cjson then cjson = require('dkjson') end local config = require("config.server") local Auth = require("src.auth") -- HTTP-Server Module local FurtServer = {} function FurtServer:new() local instance = { server = nil, port = config.port or 7811, host = config.host or "127.0.0.1", routes = {} } setmetatable(instance, self) self.__index = self return instance end -- Add route handler function FurtServer:add_route(method, path, handler) if not self.routes[method] then self.routes[method] = {} end self.routes[method][path] = handler end -- Add protected route (requires authentication) function FurtServer:add_protected_route(method, path, required_permission, handler) self:add_route(method, path, Auth.create_protected_route(required_permission, handler)) end -- Parse HTTP request function FurtServer:parse_request(client) local request_line = client:receive() if not request_line then return nil end -- Parse request line: "POST /v1/mail/send HTTP/1.1" local method, path, protocol = request_line:match("(%w+) (%S+) (%S+)") if not method then return nil end -- Parse headers local headers = {} local content_length = 0 while true do local line = client:receive() if not line or line == "" then break end local key, value = line:match("([^:]+): (.+)") if key and value then headers[key:lower()] = value if key:lower() == "content-length" then content_length = tonumber(value) or 0 end end end -- Parse body local body = "" if content_length > 0 then body = client:receive(content_length) end return { method = method, path = path, protocol = protocol, headers = headers, body = body, content_length = content_length } end -- Add CORS headers with configurable origins function FurtServer:add_cors_headers(request) local allowed_origins = config.cors and config.cors.allowed_origins or { "http://localhost:1313", "http://127.0.0.1:1313", "https://dragons-at-work.de", "https://www.dragons-at-work.de" } -- Check if request has Origin header local origin = request and request.headers and request.headers.origin local cors_origin = "*" -- Default fallback -- If origin is provided and in allowed list, use it if origin then for _, allowed in ipairs(allowed_origins) do if origin == allowed then cors_origin = origin break end end end return { ["Access-Control-Allow-Origin"] = cors_origin, ["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE, OPTIONS", ["Access-Control-Allow-Headers"] = "Content-Type, X-API-Key, Authorization, Accept", ["Access-Control-Max-Age"] = "86400", ["Access-Control-Allow-Credentials"] = "false" } end -- Create HTTP response function FurtServer:create_response(status, data, content_type, additional_headers, request) content_type = content_type or "application/json" local body = "" if type(data) == "table" then body = cjson.encode(data) else body = tostring(data or "") end -- Start with CORS headers local headers = self:add_cors_headers(request) -- Add standard headers headers["Content-Type"] = content_type headers["Content-Length"] = tostring(#body) headers["Connection"] = "close" headers["Server"] = "Furt-Lua/1.1" -- Add additional headers if provided if additional_headers then for key, value in pairs(additional_headers) do headers[key] = value end end -- Build response local response = string.format("HTTP/1.1 %d %s\r\n", status, self:get_status_text(status)) for key, value in pairs(headers) do response = response .. key .. ": " .. value .. "\r\n" end response = response .. "\r\n" .. body return response end -- Get HTTP status text function FurtServer:get_status_text(status) local status_texts = { [200] = "OK", [204] = "No Content", [400] = "Bad Request", [401] = "Unauthorized", [403] = "Forbidden", [404] = "Not Found", [405] = "Method Not Allowed", [429] = "Too Many Requests", [500] = "Internal Server Error" } return status_texts[status] or "Unknown" end -- Handle client request function FurtServer:handle_client(client) local request = self:parse_request(client) if not request then local response = self:create_response(400, {error = "Invalid request"}, nil, nil, nil) client:send(response) return end print(string.format("[%s] %s %s", os.date("%Y-%m-%d %H:%M:%S"), request.method, request.path)) -- Handle OPTIONS preflight requests (CORS) if request.method == "OPTIONS" then local response = self:create_response(204, "", "text/plain", nil, request) client:send(response) return end -- Route handling local handler = nil if self.routes[request.method] and self.routes[request.method][request.path] then handler = self.routes[request.method][request.path] end if handler then local success, result = pcall(handler, request, self) if success then client:send(result) else print("Handler error: " .. tostring(result)) local error_response = self:create_response(500, {error = "Internal server error"}, nil, nil, request) client:send(error_response) end else print("Route not found: " .. request.method .. " " .. request.path) local response = self:create_response(404, {error = "Route not found", method = request.method, path = request.path}, nil, nil, request) client:send(response) end end -- Start HTTP server function FurtServer:start() self.server = socket.bind(self.host, self.port) if not self.server then error("Failed to bind to " .. self.host .. ":" .. self.port) end local HealthRoute = require("src.routes.health") local version_info = HealthRoute.get_version_info() print(string.format("Furt HTTP-Server started on %s:%d", self.host, self.port)) print("Version: " .. version_info.version .. " (merkwerk)") 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") -- 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 local client = self.server:accept() if client then client:settimeout(10) -- 10 second timeout self:handle_client(client) client:close() end end end return FurtServer