--[[ Moonshark Lua Sandbox Environment This file contains all the Lua code needed for the sandbox environment, including core modules and utilities. It's designed to be embedded in the Go binary at build time. ]]-- __http_response = {} __module_paths = {} __module_bytecode = {} __ready_modules = {} __EXIT_SENTINEL = {} -- Unique object for exit identification -- ====================================================================== -- CORE SANDBOX FUNCTIONALITY -- ====================================================================== function exit() error(__EXIT_SENTINEL) end -- Create environment inheriting from _G function __create_env(ctx) local env = setmetatable({}, {__index = _G}) if ctx then env.ctx = ctx end if __setup_require then __setup_require(env) end return env end -- Execute script with clean environment function __execute_script(fn, ctx) __http_response = nil local env = __create_env(ctx) env.exit = exit setfenv(fn, env) local ok, result = pcall(fn) if not ok then if result == __EXIT_SENTINEL then return end error(result, 0) end return result end -- Ensure __http_response exists, then return it function __ensure_response() if not __http_response then __http_response = {} end return __http_response end -- ====================================================================== -- HTTP MODULE -- ====================================================================== local http = { -- Set HTTP status code set_status = function(code) if type(code) ~= "number" then error("http.set_status: status code must be a number", 2) end local resp = __ensure_response() resp.status = code end, -- Set HTTP header set_header = function(name, value) if type(name) ~= "string" or type(value) ~= "string" then error("http.set_header: name and value must be strings", 2) end local resp = __ensure_response() resp.headers = resp.headers or {} resp.headers[name] = value end, -- Set content type; set_header helper set_content_type = function(content_type) http.set_header("Content-Type", content_type) end, -- Set metadata (arbitrary data to be returned with response) set_metadata = function(key, value) if type(key) ~= "string" then error("http.set_metadata: key must be a string", 2) end local resp = __ensure_response() resp.metadata = resp.metadata or {} resp.metadata[key] = value end, -- HTTP client submodule client = { -- Generic request function request = function(method, url, body, options) if type(method) ~= "string" then error("http.client.request: method must be a string", 2) end if type(url) ~= "string" then error("http.client.request: url must be a string", 2) end -- Call native implementation local result = __http_request(method, url, body, options) return result end, -- Shorthand function to directly get JSON get_json = function(url, options) options = options or {} local response = http.client.get(url, options) if response.ok and response.json then return response.json end return nil, response end, -- Utility to build a URL with query parameters build_url = function(base_url, params) if not params or type(params) ~= "table" then return base_url end local query = {} for k, v in pairs(params) do if type(v) == "table" then for _, item in ipairs(v) do table.insert(query, k .. "=" .. tostring(item)) end else table.insert(query, k .. "=" .. tostring(v)) end end if #query > 0 then if base_url:find("?") then return base_url .. "&" .. table.concat(query, "&") else return base_url .. "?" .. table.concat(query, "&") end end return base_url end } } local function make_method(method, needs_body) return function(url, body_or_options, options) if needs_body then options = options or {} return http.client.request(method, url, body_or_options, options) else body_or_options = body_or_options or {} return http.client.request(method, url, nil, body_or_options) end end end http.client.get = make_method("GET", false) http.client.delete = make_method("DELETE", false) http.client.head = make_method("HEAD", false) http.client.options = make_method("OPTIONS", false) http.client.post = make_method("POST", true) http.client.put = make_method("PUT", true) http.client.patch = make_method("PATCH", true) -- ====================================================================== -- COOKIE MODULE -- ====================================================================== local cookie = { -- Set a cookie set = function(name, value, options) if type(name) ~= "string" then error("cookie.set: name must be a string", 2) end local resp = __ensure_response() resp.cookies = resp.cookies or {} local opts = options or {} local cookie = { name = name, value = value or "", path = opts.path or "/", domain = opts.domain } if opts.expires then if type(opts.expires) == "number" then if opts.expires > 0 then cookie.max_age = opts.expires local now = os.time() cookie.expires = now + opts.expires elseif opts.expires < 0 then cookie.expires = 1 cookie.max_age = 0 end -- opts.expires == 0: Session cookie (omitting both expires and max-age) end end cookie.secure = (opts.secure ~= false) cookie.http_only = (opts.http_only ~= false) if opts.same_site then local valid_values = {none = true, lax = true, strict = true} local same_site = string.lower(opts.same_site) if not valid_values[same_site] then error("cookie.set: same_site must be one of 'None', 'Lax', or 'Strict'", 2) end -- If SameSite=None, the cookie must be secure if same_site == "none" and not cookie.secure then cookie.secure = true end cookie.same_site = opts.same_site else cookie.same_site = "Lax" end table.insert(resp.cookies, cookie) return true end, -- Get a cookie value get = function(name) if type(name) ~= "string" then error("cookie.get: name must be a string", 2) end local env = getfenv(2) if env.ctx and env.ctx.cookies then return env.ctx.cookies[name] end if env.ctx and env.ctx._request_cookies then return env.ctx._request_cookies[name] end return nil end, -- Remove a cookie remove = function(name, path, domain) if type(name) ~= "string" then error("cookie.remove: name must be a string", 2) end return cookie.set(name, "", {expires = 0, path = path or "/", domain = domain}) end } -- ====================================================================== -- SESSION MODULE -- ====================================================================== local session = { get = function(key) if type(key) ~= "string" then error("session.get: key must be a string", 2) end local env = getfenv(2) if env.ctx and env.ctx.session and env.ctx.session.data then return env.ctx.session.data[key] end return nil end, set = function(key, value) if type(key) ~= "string" then error("session.set: key must be a string", 2) end if type(value) == nil then error("session.set: value cannot be nil", 2) end local resp = __ensure_response() resp.session = resp.session or {} resp.session[key] = value end, id = function() local env = getfenv(2) if env.ctx and env.ctx.session then return env.ctx.session.id end return nil end, get_all = function() local env = getfenv(2) if env.ctx and env.ctx.session then return env.ctx.session.data end return nil end } -- ====================================================================== -- CSRF MODULE -- ====================================================================== local csrf = { generate = function() local token = util.generate_token(32) session.set("_csrf_token", token) return token end, field = function() local token = session.get("_csrf_token") if not token then token = csrf.generate() end return string.format('', token) end, validate = function() local env = getfenv(2) local token = false if env.ctx and env.ctx.session and env.ctx.session.data then token = env.ctx.session.data["_csrf_token"] end if not token then http.set_status(403) __http_response.body = "CSRF validation failed" exit() end local request_token = nil if env.ctx and env.ctx.form then request_token = env.ctx.form._csrf_token end if not request_token and env.ctx and env.ctx._request_headers then request_token = env.ctx._request_headers["x-csrf-token"] or env.ctx._request_headers["csrf-token"] end if not request_token or request_token ~= token then http.set_status(403) __http_response.body = "CSRF validation failed" exit() end return true end } -- ====================================================================== -- UTIL MODULE -- ====================================================================== -- Utility module implementation local util = { generate_token = function(length) return __generate_token(length or 32) end, -- Deep copy of tables deep_copy = function(obj) if type(obj) ~= 'table' then return obj end local res = {} for k, v in pairs(obj) do res[k] = util.deep_copy(v) end return res end, -- Merge tables merge_tables = function(t1, t2) if type(t1) ~= 'table' or type(t2) ~= 'table' then error("Both arguments must be tables", 2) end local result = util.deep_copy(t1) for k, v in pairs(t2) do if type(v) == 'table' and type(result[k]) == 'table' then result[k] = util.merge_tables(result[k], v) else result[k] = v end end return result end, -- String utilities string = { -- Trim whitespace trim = function(s) return (s:gsub("^%s*(.-)%s*$", "%1")) end, -- Split string split = function(s, delimiter) delimiter = delimiter or "," local result = {} for match in (s..delimiter):gmatch("(.-)"..delimiter) do table.insert(result, match) end return result end } } -- ====================================================================== -- TEMPLATE RENDER FUNCTION -- ====================================================================== _G.render = function(template_str, env) local OPEN_TAG, CLOSE_TAG = "" -- Helper functions local function escape_html(s) local entities = {['&']='&', ['<']='<', ['>']='>', ['"']='"', ["'"]='''} return (s:gsub([=[["><'&]]=], entities)) end local function get_line(s, ln) for line in s:gmatch("([^\n]*)\n?") do if ln == 1 then return line end ln = ln - 1 end end local function pos_to_line(s, pos) local line = 1 for _ in s:sub(1, pos):gmatch("\n") do line = line + 1 end return line end -- Parse template local pos, chunks = 1, {} while pos <= #template_str do local start, stop = template_str:find(OPEN_TAG, pos, true) if not start then table.insert(chunks, template_str:sub(pos)) break end if start > pos then table.insert(chunks, template_str:sub(pos, start-1)) end pos = stop + 1 local modifier = template_str:match("^[=-]", pos) if modifier then pos = pos + 1 end local close_start, close_stop = template_str:find(CLOSE_TAG, pos, true) if not close_start then error("Failed to find closing tag at position " .. pos) end local trim_newline = false if template_str:sub(close_start-1, close_start-1) == "-" then close_start = close_start - 1 trim_newline = true end local code = template_str:sub(pos, close_start-1) table.insert(chunks, {modifier or "code", code, pos}) pos = close_stop + 1 if trim_newline and template_str:sub(pos, pos) == "\n" then pos = pos + 1 end end -- Compile chunks to Lua code local buffer = {"local _tostring, _escape, _b, _b_i = ...\n"} for _, chunk in ipairs(chunks) do local t = type(chunk) if t == "string" then table.insert(buffer, "_b_i = _b_i + 1\n") table.insert(buffer, "_b[_b_i] = " .. string.format("%q", chunk) .. "\n") else t = chunk[1] if t == "code" then table.insert(buffer, "--[[" .. chunk[3] .. "]] " .. chunk[2] .. "\n") elseif t == "=" or t == "-" then table.insert(buffer, "_b_i = _b_i + 1\n") table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = ") if t == "=" then table.insert(buffer, "_escape(_tostring(" .. chunk[2] .. "))\n") else table.insert(buffer, "_tostring(" .. chunk[2] .. ")\n") end end end end table.insert(buffer, "return _b") -- Load the compiled code local fn, err = loadstring(table.concat(buffer)) if not fn then error(err) end -- Create execution environment env = env or {} local runtime_env = setmetatable({}, {__index = function(_, k) return env[k] or _G[k] end}) setfenv(fn, runtime_env) local output_buffer = {} fn(tostring, escape_html, output_buffer, 0) return table.concat(output_buffer) end -- ====================================================================== -- REGISTER MODULES GLOBALLY -- ====================================================================== _G.http = http _G.session = session _G.csrf = csrf _G.cookie = cookie _G.util = util