Moonshark/runner/lua/sandbox.lua

423 lines
11 KiB
Lua

-- Simplified sandbox.lua - Global function approach
function __execute(script_func, ctx, response)
-- Store context and response globally for function access
__ctx = ctx
__response = response
_G.ctx = ctx
-- Execute script in global environment
local ok, result = pcall(script_func)
-- Clean up
__ctx = nil
__response = nil
if not ok then
if result == "__EXIT__" then
return {nil, response}
end
error(result, 0)
end
return {result, response}
end
-- Exit sentinel
function exit()
error("__EXIT__")
end
-- ======================================================================
-- HTTP RESPONSE FUNCTIONS
-- ======================================================================
function http_set_status(code)
__response.status = code
end
function http_set_header(name, value)
__response.headers = __response.headers or {}
__response.headers[name] = value
end
function http_set_content_type(ct)
__response.headers = __response.headers or {}
__response.headers["Content-Type"] = ct
end
function http_set_metadata(key, value)
__response.metadata = __response.metadata or {}
__response.metadata[key] = value
end
function http_redirect(url, status)
__response.status = status or 302
__response.headers = __response.headers or {}
__response.headers["Location"] = url
error("__EXIT__")
end
-- ======================================================================
-- COOKIE FUNCTIONS
-- ======================================================================
function cookie_set(name, value, options)
__response.cookies = __response.cookies or {}
local opts = options or {}
local cookie = {
name = name,
value = value or "",
path = opts.path or "/",
domain = opts.domain,
secure = opts.secure ~= false,
http_only = opts.http_only ~= false
}
if opts.expires and opts.expires > 0 then
cookie.max_age = opts.expires
end
table.insert(__response.cookies, cookie)
end
function cookie_get(name)
return __ctx._request_cookies and __ctx._request_cookies[name]
end
function cookie_delete(name, path, domain)
return cookie_set(name, "", {expires = -1, path = path or "/", domain = domain})
end
-- ======================================================================
-- SESSION FUNCTIONS
-- ======================================================================
function session_set(key, value)
__response.session = __response.session or {}
__response.session[key] = value
if __ctx.session and __ctx.session.data then
__ctx.session.data[key] = value
end
end
function session_get(key)
return __ctx.session and __ctx.session.data and __ctx.session.data[key]
end
function session_id()
return __ctx.session and __ctx.session.id
end
function session_get_all()
if __ctx.session and __ctx.session.data then
local copy = {}
for k, v in pairs(__ctx.session.data) do
copy[k] = v
end
return copy
end
return {}
end
function session_delete(key)
__response.session = __response.session or {}
__response.session[key] = "__DELETE__"
if __ctx.session and __ctx.session.data then
__ctx.session.data[key] = nil
end
end
function session_clear()
__response.session = {__clear_all = true}
if __ctx.session and __ctx.session.data then
for k in pairs(__ctx.session.data) do
__ctx.session.data[k] = nil
end
end
end
-- ======================================================================
-- CSRF FUNCTIONS
-- ======================================================================
function csrf_generate()
local token = generate_token(32)
session_set("_csrf_token", token)
return token
end
function csrf_field()
local token = session_get("_csrf_token")
if not token then
token = csrf_generate()
end
return string.format('<input type="hidden" name="_csrf_token" value="%s" />',
html_special_chars(token))
end
function csrf_validate()
local token = __ctx.session and __ctx.session.data and __ctx.session.data["_csrf_token"]
if not token then
__response.status = 403
error("__EXIT__")
end
local request_token = (__ctx._request_form and __ctx._request_form._csrf_token) or
(__ctx._request_headers and (__ctx._request_headers["x-csrf-token"] or __ctx._request_headers["csrf-token"]))
if not request_token or request_token ~= token then
__response.status = 403
error("__EXIT__")
end
return true
end
-- ======================================================================
-- CONTENT TYPE HELPERS
-- ======================================================================
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
-- ======================================================================
-- TEMPLATE RENDER FUNCTIONS
-- ======================================================================
-- Template processing with code execution
function render(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 unescaped_start = template_str:find("{{{", pos, true)
local escaped_start = template_str:find("{{", pos, true)
local start, tag_type, open_len
if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
start, tag_type, open_len = unescaped_start, "-", 3
elseif escaped_start then
start, tag_type, open_len = escaped_start, "=", 2
else
table.insert(chunks, template_str:sub(pos))
break
end
if start > pos then
table.insert(chunks, template_str:sub(pos, start-1))
end
pos = start + open_len
local close_tag = 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 code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$")
-- Check if it's a simple variable name for escaped output
local is_simple_var = tag_type == "=" and code:match("^[%w_]+$")
table.insert(chunks, {tag_type, code, pos, is_simple_var})
pos = close_stop + 1
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 == "=" then
if chunk[4] then -- is_simple_var
table.insert(buffer, "_b_i = _b_i + 1\n")
table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = _escape(_tostring(" .. chunk[2] .. "))\n")
else
table.insert(buffer, "--[[" .. chunk[3] .. "]] " .. chunk[2] .. "\n")
end
elseif t == "-" then
table.insert(buffer, "_b_i = _b_i + 1\n")
table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = _tostring(" .. chunk[2] .. ")\n")
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, html_special_chars, output_buffer, 0)
return table.concat(output_buffer)
end
-- Named placeholder processing
function parse(template_str, env)
local pos, output = 1, {}
env = env or {}
while pos <= #template_str do
local unescaped_start, unescaped_end, unescaped_name = template_str:find("{{{%s*([%w_]+)%s*}}}", pos)
local escaped_start, escaped_end, escaped_name = template_str:find("{{%s*([%w_]+)%s*}}", pos)
local next_pos, placeholder_end, name, escaped
if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false
elseif escaped_start then
next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true
else
local text = template_str:sub(pos)
if text and #text > 0 then
table.insert(output, text)
end
break
end
local text = template_str:sub(pos, next_pos - 1)
if text and #text > 0 then
table.insert(output, text)
end
local value = env[name]
local str = tostring(value or "")
if escaped then
str = html_special_chars(str)
end
table.insert(output, str)
pos = placeholder_end + 1
end
return table.concat(output)
end
-- Indexed placeholder processing
function iparse(template_str, values)
local pos, output, value_index = 1, {}, 1
values = values or {}
while pos <= #template_str do
local unescaped_start, unescaped_end = template_str:find("{{{}}}", pos, true)
local escaped_start, escaped_end = template_str:find("{{}}", pos, true)
local next_pos, placeholder_end, escaped
if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then
next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false
elseif escaped_start then
next_pos, placeholder_end, escaped = escaped_start, escaped_end, true
else
local text = template_str:sub(pos)
if text and #text > 0 then
table.insert(output, text)
end
break
end
local text = template_str:sub(pos, next_pos - 1)
if text and #text > 0 then
table.insert(output, text)
end
local value = values[value_index]
local str = tostring(value or "")
if escaped then
str = html_special_chars(str)
end
table.insert(output, str)
pos = placeholder_end + 1
value_index = value_index + 1
end
return table.concat(output)
end
-- ======================================================================
-- PASSWORD FUNCTIONS
-- ======================================================================
-- 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