-- furt-lua/src/main.lua -- Pure Lua HTTP-Server for Furt API-Gateway -- Dragons@Work Digital Sovereignty Project local socket = require("socket") local cjson = require("cjson") -- Load configuration local config = require("config.server") -- HTTP-Server Module local FurtServer = {} function FurtServer:new() local instance = { server = nil, port = config.port or 8080, 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 -- 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.0" -- 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", [404] = "Not Found", [405] = "Method Not Allowed", [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) 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 print(string.format("Furt HTTP-Server started on %s:%d", self.host, self.port)) print("CORS enabled for all 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 -- Initialize server and routes local server = FurtServer:new() -- Health check route server:add_route("GET", "/health", function(request) local response_data = { status = "healthy", service = "furt-lua", version = "1.0.0", timestamp = os.time(), smtp_configured = config.mail and config.mail.username ~= nil } return server:create_response(200, response_data, nil, nil, request) end) -- Test route for development server:add_route("POST", "/test", function(request) local response_data = { message = "Test endpoint working", received_data = request.body, headers_count = 0 } -- Count headers for _ in pairs(request.headers) do response_data.headers_count = response_data.headers_count + 1 end return server:create_response(200, response_data, nil, nil, request) end) -- Mail service route (placeholder for Week 1) server:add_route("POST", "/v1/mail/send", function(request) print("Mail endpoint called - Method: " .. request.method .. ", Path: " .. request.path) -- Basic validation if not request.body or request.body == "" then return server:create_response(400, {error = "No request body"}, nil, nil, request) end -- Try to parse JSON local success, data = pcall(cjson.decode, request.body) if not success then return server:create_response(400, {error = "Invalid JSON", body = request.body}, nil, nil, request) end -- Basic field validation if not data.name or not data.email or not data.message then return server:create_response(400, { error = "Missing required fields", required = {"name", "email", "message"}, received = data }, nil, nil, request) end -- Send email via SMTP local SMTP = require("src.smtp") local smtp_client = SMTP:new(config.mail) local request_id = os.time() .. "-" .. math.random(1000, 9999) local subject = data.subject or "Contact Form Message" local email_content = string.format("From: %s <%s>\nSubject: %s\n\n%s", data.name, data.email, subject, data.message) local success, result = smtp_client:send_email( config.mail.to_address, subject, email_content, data.name) if success then return server:create_response(200, { success = true, message = "Mail sent successfully", request_id = request_id }, nil, nil, request) else print("SMTP Error: " .. tostring(result)) return server:create_response(500, { success = false, error = "Failed to send email: " .. tostring(result), request_id = request_id }, nil, nil, request) end end) -- Start server server:start()