local json = require("json") local http = {} -- Global routing tables _G._http_routes = _G._http_routes or {} _G._http_middleware = _G._http_middleware or {} -- Request/Response classes local Request = {} Request.__index = Request local Response = {} Response.__index = Response -- ====================================================================== -- ENHANCED COOKIE PARSING -- ====================================================================== local function parse_cookies(cookie_header) local cookies = {} if string.is_empty(cookie_header) then return cookies end -- Split by semicolon and parse each cookie local cookie_pairs = string.split(cookie_header, ";") for _, cookie_pair in ipairs(cookie_pairs) do local trimmed = string.trim(cookie_pair) if not string.is_empty(trimmed) then local parts = string.split(trimmed, "=") if #parts >= 2 then local name = string.trim(parts[1]) local value = string.trim(parts[2]) -- URL decode the value local success, decoded = pcall(function() return string.url_decode(value) end) cookies[name] = success and decoded or value elseif #parts == 1 then -- Cookie without value cookies[string.trim(parts[1])] = "" end end end return cookies end -- ====================================================================== -- ENHANCED ROUTER IMPLEMENTATION -- ====================================================================== local function split_path(path) if string.is_empty(path) or path == "/" then return {} end -- Remove leading/trailing slashes and split local clean_path = string.trim(path, "/") if string.is_empty(clean_path) then return {} end return string.split(clean_path, "/") end local function match_route(method, path) local path_segments = split_path(path) for _, route in ipairs(_G._http_routes) do if route.method == method then local params = {} local route_segments = route.segments local match = true local i = 1 while i <= #route_segments and match do local route_seg = route_segments[i] if route_seg == "*" then -- Wildcard captures everything remaining local remaining = {} for j = i, #path_segments do table.insert(remaining, path_segments[j]) end params["*"] = string.join(remaining, "/") break elseif string.starts_with(route_seg, ":") then -- Parameter segment if i <= #path_segments then local param_name = string.slice(route_seg, 2, -1) params[param_name] = path_segments[i] else match = false end else -- Static segment if i > #path_segments or route_seg ~= path_segments[i] then match = false end end i = i + 1 end -- Must consume all segments unless wildcard if match and (i > #path_segments or (route_segments[i-1] and route_segments[i-1] == "*")) then return route, params end end end return nil, {} end function _http_handle_request(req_table, res_table) local req = Request.new(req_table) local res = Response.new(res_table) -- Execute middleware chain first local function run_middleware(index) if index > #_G._http_middleware then local route, params = match_route(req.method, req.path) req.params = params if not route then res:status(404):send("Not Found") return end -- Run route handler route.handler(req, res) return end local mw = _G._http_middleware[index] if mw.path == nil or string.starts_with(req.path, mw.path) then mw.handler(req, res, function() run_middleware(index + 1) end) else run_middleware(index + 1) end end run_middleware(1) end -- Functions for route synchronization function _http_get_routes() return { routes = _G._http_routes, middleware = _G._http_middleware } end function _http_sync_worker_routes() local data = _G._http_routes_data if data and data.routes and data.middleware then _G._http_routes = data.routes _G._http_middleware = data.middleware end end -- ====================================================================== -- SERVER CLASS -- ====================================================================== local Server = {} Server.__index = Server function http.server() if _G.__IS_WORKER then return setmetatable({}, Server) end local server = setmetatable({ _server_created = false }, Server) local success, err = moonshark.http_create_server() if not success then error("Failed to create HTTP server: " .. (err or "unknown error")) end server._server_created = true return server end function Server:use(...) local args = {...} if #args == 1 and type(args[1]) == "function" then table.insert(_G._http_middleware, {path = nil, handler = args[1]}) elseif #args == 2 and type(args[1]) == "string" and type(args[2]) == "function" then table.insert(_G._http_middleware, {path = args[1], handler = args[2]}) else error("Invalid arguments to use()") end return self end function Server:_add_route(method, path, handler) -- Ensure path starts with / if not string.starts_with(path, "/") then path = "/" .. path end local segments = split_path(path) table.insert(_G._http_routes, { method = method, path = path, segments = segments, handler = handler }) return self end -- HTTP method helpers function Server:get(path, handler) return self:_add_route("GET", path, handler) end function Server:post(path, handler) return self:_add_route("POST", path, handler) end function Server:put(path, handler) return self:_add_route("PUT", path, handler) end function Server:delete(path, handler) return self:_add_route("DELETE", path, handler) end function Server:patch(path, handler) return self:_add_route("PATCH", path, handler) end function Server:head(path, handler) return self:_add_route("HEAD", path, handler) end function Server:options(path, handler) return self:_add_route("OPTIONS", path, handler) end function Server:all(path, handler) local methods = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"} for _, method in ipairs(methods) do self:_add_route(method, path, handler) end return self end function Server:listen(port, host, callback) -- Workers should not listen if _G.__IS_WORKER then if callback then callback() end return self end if type(host) == "function" then callback = host host = "localhost" end host = host or "localhost" local addr = host .. ":" .. tostring(port) -- Spawn workers first local success, err = moonshark.http_spawn_workers() if not success then error("Failed to spawn workers: " .. (err or "unknown error")) end -- Then start listening success, err = moonshark.http_listen(addr) if not success then error("Failed to start server: " .. (err or "unknown error")) end if callback then callback() end return self end function Server:close() if _G.__IS_WORKER then return true end return moonshark.http_close_server() end -- ====================================================================== -- ENHANCED REQUEST OBJECT -- ====================================================================== function Request.new(req_table) local req = setmetatable({ method = req_table.method, path = req_table.path, query = req_table.query or {}, headers = req_table.headers or {}, params = {}, body = req_table.body or "", cookies = {} }, Request) local cookie_header = req.headers["Cookie"] or req.headers["cookie"] if cookie_header then req.cookies = parse_cookies(cookie_header) end return req end function Request:get(header_name) local lower_name = string.lower(header_name) return self.headers[header_name] or self.headers[lower_name] end function Request:header(header_name) return self:get(header_name) end function Request:param(name, default_value) return self.params[name] or default_value end function Request:query_param(name, default_value) return self.query[name] or default_value end function Request:cookie(name, default_value) return self.cookies[name] or default_value end function Request:has_cookie(name) return self.cookies[name] ~= nil end function Request:get_cookies() -- Return a copy to prevent modification local copy = {} for k, v in pairs(self.cookies) do copy[k] = v end return copy end function Request:cookie_matches(name, pattern) local cookie_value = self.cookies[name] if not cookie_value then return false end return string.match(pattern, cookie_value) end function Request:get_cookies_by_names(names) local result = {} for _, name in ipairs(names) do result[name] = self.cookies[name] end return result end function Request:has_auth_cookies() local auth_cookie_names = {"session", "token", "auth", "jwt", "access_token"} for _, name in ipairs(auth_cookie_names) do if self.cookies[name] then return true end end return false end function Request:json() if string.is_empty(self.body) then return nil end local success, result = pcall(function() return json.decode(self.body) end) if success then return result else error("Invalid JSON in request body") end end function Request:is_json() local content_type = self:get("content-type") or "" return string.contains(content_type, "application/json") end function Request:is_form() local content_type = self:get("content-type") or "" return string.contains(content_type, "application/x-www-form-urlencoded") end function Request:is_multipart() local content_type = self:get("content-type") or "" return string.contains(content_type, "multipart/form-data") end function Request:is_xml() local content_type = self:get("content-type") or "" return string.contains(content_type, "application/xml") or string.contains(content_type, "text/xml") end function Request:accepts(mime_type) local accept_header = self:get("accept") or "" return string.contains(accept_header, mime_type) or string.contains(accept_header, "*/*") end function Request:user_agent() return self:get("user-agent") or "" end function Request:ip() return self:get("x-forwarded-for") or self:get("x-real-ip") or "unknown" end function Request:is_secure() local proto = self:get("x-forwarded-proto") return proto == "https" or string.starts_with(self:get("host") or "", "https://") end -- ====================================================================== -- ENHANCED RESPONSE OBJECT -- ====================================================================== function Response.new(res_table) local res = setmetatable({ _table = res_table, _sent = false }, Response) return res end function Response:status(code) if self._sent then error("Cannot set status after response has been sent") end self._table.status = code return self end function Response:header(name, value) if self._sent then error("Cannot set headers after response has been sent") end self._table.headers[name] = value return self end function Response:set(name, value) return self:header(name, value) end function Response:type(content_type) return self:header("Content-Type", content_type) end function Response:send(data) if self._sent then error("Response already sent") end if type(data) == "table" then self:json(data) elseif type(data) == "number" then self._table.status = data self._table.body = "" else self._table.body = tostring(data or "") end self._sent = true return self end function Response:json(data) if self._sent then error("Response already sent") end self:type("application/json; charset=utf-8") local success, json_str = pcall(function() return json.encode(data) end) if success then self._table.body = json_str else error("Failed to encode JSON response") end self._sent = true return self end function Response:text(text) if self._sent then error("Response already sent") end self:type("text/plain; charset=utf-8") self._table.body = tostring(text or "") self._sent = true return self end function Response:html(html) if self._sent then error("Response already sent") end self:type("text/html; charset=utf-8") self._table.body = tostring(html or "") self._sent = true return self end function Response:xml(xml) if self._sent then error("Response already sent") end self:type("application/xml; charset=utf-8") self._table.body = tostring(xml or "") self._sent = true return self end function Response:redirect(url, status) if self._sent then error("Response already sent") end status = status or 302 self:status(status) self:header("Location", url) self._table.body = "" self._sent = true return self end function Response:cookie(name, value, options) if self._sent then error("Cannot set cookies after response has been sent") end options = options or {} local cookie_value = tostring(value) -- URL encode the cookie value if it contains special characters if string.match("[;,\\s]", cookie_value) then cookie_value = string.url_encode(cookie_value) end local cookie = name .. "=" .. cookie_value if options.expires then cookie = cookie .. "; Expires=" .. options.expires end if options.max_age then cookie = cookie .. "; Max-Age=" .. tostring(options.max_age) end if options.domain then cookie = cookie .. "; Domain=" .. options.domain end if options.path then cookie = cookie .. "; Path=" .. options.path end if options.secure then cookie = cookie .. "; Secure" end if options.http_only then cookie = cookie .. "; HttpOnly" end if options.same_site then cookie = cookie .. "; SameSite=" .. options.same_site end local existing = self._table.headers["Set-Cookie"] if existing then if type(existing) == "table" then table.insert(existing, cookie) else self._table.headers["Set-Cookie"] = {existing, cookie} end else self._table.headers["Set-Cookie"] = cookie end return self end function Response:clear_cookie(name, options) options = options or {} options.expires = "Thu, 01 Jan 1970 00:00:00 GMT" options.max_age = 0 return self:cookie(name, "", options) end function Response:attachment(filename) if filename then self:header("Content-Disposition", 'attachment; filename="' .. filename .. '"') else self:header("Content-Disposition", "attachment") end return self end function Response:download(data, filename, content_type) if filename then self:attachment(filename) end if content_type then self:type(content_type) elseif filename then -- Try to guess content type from extension if string.ends_with(filename, ".pdf") then self:type("application/pdf") elseif string.ends_with(filename, ".zip") then self:type("application/zip") elseif string.ends_with(filename, ".json") then self:type("application/json") elseif string.ends_with(filename, ".csv") then self:type("text/csv") else self:type("application/octet-stream") end else self:type("application/octet-stream") end return self:send(data) end -- ====================================================================== -- MIDDLEWARE HELPERS -- ====================================================================== function http.cors(options) options = options or {} local origin = options.origin or "*" local methods = options.methods or "GET,HEAD,PUT,PATCH,POST,DELETE" local headers = options.headers or "Content-Type,Authorization" return function(req, res, next) res:header("Access-Control-Allow-Origin", origin) res:header("Access-Control-Allow-Methods", methods) res:header("Access-Control-Allow-Headers", headers) if options.credentials then res:header("Access-Control-Allow-Credentials", "true") end if string.iequals(req.method, "OPTIONS") then res:status(204):send("") else next() end end end function http.static(root_path, url_prefix) url_prefix = url_prefix or "/" -- Ensure prefix starts with / if not string.starts_with(url_prefix, "/") then url_prefix = "/" .. url_prefix end if not _G.__IS_WORKER then local success, err = moonshark.http_register_static(url_prefix, root_path) if not success then error("Failed to register static handler: " .. (err or "unknown error")) end end -- Return no-op middleware return function(req, res, next) next() end end function http.json_parser() return function(req, res, next) if req:is_json() and not string.is_empty(req.body) then local success, data = pcall(function() return req:json() end) if success then req.json_body = data else res:status(400):json({error = "Invalid JSON"}) return end end next() end end function http.logger(format) format = format or ":method :path :status :response-time ms" return function(req, res, next) local start_time = os.clock() next() local duration = (os.clock() - start_time) * 1000 local status = res._table.status or 200 local log_message = string.template(format, { method = req.method, path = req.path, status = status, ["response-time"] = string.format("%.2f", duration), ["user-agent"] = req:user_agent(), ip = req:ip() }) print(log_message) end end function http.compression() return function(req, res, next) local accept_encoding = req:get("accept-encoding") or "" if string.contains(accept_encoding, "gzip") then res:header("Content-Encoding", "gzip") elseif string.contains(accept_encoding, "deflate") then res:header("Content-Encoding", "deflate") end next() end end function http.security() return function(req, res, next) res:header("X-Content-Type-Options", "nosniff") res:header("X-Frame-Options", "DENY") res:header("X-XSS-Protection", "1; mode=block") res:header("Strict-Transport-Security", "max-age=31536000; includeSubDomains") res:header("Referrer-Policy", "strict-origin-when-cross-origin") next() end end function http.rate_limit(options) options = options or {} local max_requests = options.max or 100 local window_ms = options.window or 60000 -- 1 minute local clients = {} return function(req, res, next) local client_ip = req:ip() local now = os.time() * 1000 if not clients[client_ip] then clients[client_ip] = {count = 1, reset_time = now + window_ms} else local client = clients[client_ip] if now > client.reset_time then client.count = 1 client.reset_time = now + window_ms else client.count = client.count + 1 if client.count > max_requests then res:status(429):json({ error = "Too many requests", retry_after = math.ceil((client.reset_time - now) / 1000) }) return end end end next() end end function http.create_server(callback) local app = http.server() if callback then callback(app) end return app end return http