--[[ sandbox.lua ]]-- __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) http.redirect = function(url, status) if type(url) ~= "string" then error("http.redirect: url must be a string", 2) end status = status or 302 -- Default to temporary redirect local resp = __ensure_response() resp.status = status resp.headers = resp.headers or {} resp.headers["Location"] = url exit() end -- ====================================================================== -- 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, delete = function(key) if type(key) ~= "string" then error("session.delete: key must be a string", 2) end local resp = __ensure_response() resp.session = resp.session or {} resp.session[key] = "__SESSION_DELETE_MARKER__" local env = getfenv(2) if env.ctx and env.ctx.session and env.ctx.session.data then env.ctx.session.data[key] = nil end end, clear = function() local env = getfenv(2) if env.ctx and env.ctx.session and env.ctx.session.data then for k, _ in pairs(env.ctx.session.data) do env.ctx.session.data[k] = nil end end local resp = __ensure_response() resp.session = {} resp.session["__clear_all"] = true 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 } -- ====================================================================== -- TEMPLATE RENDER FUNCTIONS -- ====================================================================== -- Shared utilities local __template_util = { escape_html = function(s) local entities = {['&']='&', ['<']='<', ['>']='>', ['"']='"', ["'"]='''} return (s:gsub([=[["><'&]]=], entities)) end, build_output = function(chunks) return table.concat(chunks) end, add_text = function(output, text) if text and #text > 0 then table.insert(output, text) end end, add_value = function(output, value, escaped) local str = tostring(value or "") table.insert(output, escaped and __template_util.escape_html(str) or str) end } -- Template processing with code execution _G.render = function(template_str, env) 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 local pos, chunks = 1, {} while pos <= #template_str do local escaped_start = template_str:find("<%?", pos, true) local unescaped_start = template_str:find(" pos then table.insert(chunks, template_str:sub(pos, start-1)) end pos = start + open_len local close_tag = tag_type == "=" and "?>" or tag_type == "-" and "!>" or "%>" 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, {tag_type, code, pos}) pos = close_stop + 1 if trim_newline and template_str:sub(pos, pos) == "\n" then pos = pos + 1 end end 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") local fn, err = loadstring(table.concat(buffer)) if not fn then error(err) end 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, __template_util.escape_html, output_buffer, 0) return table.concat(output_buffer) end -- Named placeholder processing _G.parse = function(template_str, env) local pos, output = 1, {} env = env or {} while pos <= #template_str do local escaped_start, escaped_end, escaped_name = template_str:find("<%?%s*([%w_]+)%s*%?>", pos) local unescaped_start, unescaped_end, unescaped_name = template_str:find("", pos) local next_pos, placeholder_end, name, escaped if escaped_start and (not unescaped_start or escaped_start < unescaped_start) then next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true elseif unescaped_start then next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false else __template_util.add_text(output, template_str:sub(pos)) break end __template_util.add_text(output, template_str:sub(pos, next_pos - 1)) __template_util.add_value(output, env[name], escaped) pos = placeholder_end + 1 end return __template_util.build_output(output) end -- Indexed placeholder processing _G.iparse = function(template_str, values) local pos, output, value_index = 1, {}, 1 values = values or {} while pos <= #template_str do local escaped_start, escaped_end = template_str:find("<%?>", pos, true) local unescaped_start, unescaped_end = template_str:find("", pos, true) local next_pos, placeholder_end, escaped if escaped_start and (not unescaped_start or escaped_start < unescaped_start) then next_pos, placeholder_end, escaped = escaped_start, escaped_end, true elseif unescaped_start then next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false else __template_util.add_text(output, template_str:sub(pos)) break end __template_util.add_text(output, template_str:sub(pos, next_pos - 1)) __template_util.add_value(output, values[value_index], escaped) pos = placeholder_end + 1 value_index = value_index + 1 end return __template_util.build_output(output) end -- ====================================================================== -- PASSWORD MODULE -- ====================================================================== local password = {} -- Hash a password using Argon2id -- Options: -- memory: Amount of memory to use in KB (default: 128MB) -- iterations: Number of iterations (default: 4) -- parallelism: Number of threads (default: 4) -- salt_length: Length of salt in bytes (default: 16) -- key_length: Length of the derived key in bytes (default: 32) function password.hash(plain_password, options) if type(plain_password) ~= "string" then error("password.hash: expected string password", 2) end return __password_hash(plain_password, options) end -- Verify a password against a hash function password.verify(plain_password, hash_string) if type(plain_password) ~= "string" then error("password.verify: expected string password", 2) end if type(hash_string) ~= "string" then error("password.verify: expected string hash", 2) end return __password_verify(plain_password, hash_string) end -- ====================================================================== -- SEND MODULE -- ====================================================================== local send = {} function send.html(content) http.set_content_type("text/html") return content end function send.json(content) http.set_content_type("application/json") return content end function send.text(content) http.set_content_type("text/plain") return content end function send.xml(content) http.set_content_type("application/xml") return content end function send.javascript(content) http.set_content_type("application/javascript") return content end function send.css(content) http.set_content_type("text/css") return content end function send.svg(content) http.set_content_type("image/svg+xml") return content end function send.csv(content) http.set_content_type("text/csv") return content end function send.binary(content, mime_type) http.set_content_type(mime_type or "application/octet-stream") return content end -- ====================================================================== -- REGISTER MODULES GLOBALLY -- ====================================================================== _G.http = http _G.session = session _G.csrf = csrf _G.cookie = cookie _G.password = password _G.send = send