593 lines
15 KiB
Lua
593 lines
15 KiB
Lua
-- sandbox.lua
|
|
|
|
function __execute(script_func, ctx, response)
|
|
-- Store context and response globally for function access
|
|
__ctx = ctx
|
|
__response = response
|
|
_G.ctx = ctx
|
|
|
|
-- Create a coroutine for script execution to handle early exits
|
|
local co = coroutine.create(function()
|
|
return script_func()
|
|
end)
|
|
|
|
local ok, result = coroutine.resume(co)
|
|
|
|
-- Clean up
|
|
__ctx = nil
|
|
__response = nil
|
|
|
|
if not ok then
|
|
-- Real error during script execution
|
|
error(result, 0)
|
|
end
|
|
|
|
-- Check if exit was requested
|
|
if result == "__EXIT__" then
|
|
return {nil, response}
|
|
end
|
|
|
|
return {result, response}
|
|
end
|
|
|
|
-- Exit sentinel using coroutine yield instead of error
|
|
function exit()
|
|
coroutine.yield("__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
|
|
coroutine.yield("__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
|
|
coroutine.yield("__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
|
|
coroutine.yield("__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 is_control_structure(code)
|
|
-- Check if code is a control structure that doesn't produce output
|
|
local trimmed = code:match("^%s*(.-)%s*$")
|
|
return trimmed == "else" or
|
|
trimmed == "end" or
|
|
trimmed:match("^if%s") or
|
|
trimmed:match("^elseif%s") or
|
|
trimmed:match("^for%s") or
|
|
trimmed:match("^while%s") or
|
|
trimmed:match("^repeat%s*$") or
|
|
trimmed:match("^until%s") or
|
|
trimmed:match("^do%s*$") or
|
|
trimmed:match("^local%s") or
|
|
trimmed:match("^function%s") or
|
|
trimmed:match(".*=%s*function%s*%(") or
|
|
trimmed:match(".*then%s*$") or
|
|
trimmed:match(".*do%s*$")
|
|
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*$")
|
|
local is_control = is_control_structure(code)
|
|
|
|
table.insert(chunks, {tag_type, code, pos, is_control})
|
|
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
|
|
local tag_type, code, pos, is_control = chunk[1], chunk[2], chunk[3], chunk[4]
|
|
|
|
if is_control then
|
|
-- Control structure - just insert as raw Lua code
|
|
table.insert(buffer, "--[[" .. pos .. "]] " .. code .. "\n")
|
|
elseif tag_type == "=" then
|
|
-- Simple variable check
|
|
if code:match("^[%w_]+$") then
|
|
table.insert(buffer, "_b_i = _b_i + 1\n")
|
|
table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n")
|
|
else
|
|
-- Expression output with escaping
|
|
table.insert(buffer, "_b_i = _b_i + 1\n")
|
|
table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _escape(_tostring(" .. code .. "))\n")
|
|
end
|
|
elseif tag_type == "-" then
|
|
-- Unescaped output
|
|
table.insert(buffer, "_b_i = _b_i + 1\n")
|
|
table.insert(buffer, "--[[" .. pos .. "]] _b[_b_i] = _tostring(" .. code .. ")\n")
|
|
end
|
|
end
|
|
end
|
|
table.insert(buffer, "return _b")
|
|
|
|
local generated_code = table.concat(buffer)
|
|
|
|
-- DEBUG: Uncomment to see generated code
|
|
-- print("Generated Lua code:")
|
|
-- print(generated_code)
|
|
-- print("---")
|
|
|
|
local fn, err = loadstring(generated_code)
|
|
if not fn then
|
|
print("Generated code that failed to compile:")
|
|
print(generated_code)
|
|
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
|
|
|
|
-- ======================================================================
|
|
-- SESSION FLASH FUNCTIONS
|
|
-- ======================================================================
|
|
|
|
function session_flash(key, value)
|
|
__response.flash = __response.flash or {}
|
|
__response.flash[key] = value
|
|
end
|
|
|
|
function session_get_flash(key)
|
|
-- Check current flash data first
|
|
if __response.flash and __response.flash[key] ~= nil then
|
|
return __response.flash[key]
|
|
end
|
|
|
|
-- Check session flash data
|
|
if __ctx.session and __ctx.session.flash and __ctx.session.flash[key] ~= nil then
|
|
return __ctx.session.flash[key]
|
|
end
|
|
|
|
return nil
|
|
end
|
|
|
|
function session_has_flash(key)
|
|
-- Check current flash
|
|
if __response.flash and __response.flash[key] ~= nil then
|
|
return true
|
|
end
|
|
|
|
-- Check session flash
|
|
if __ctx.session and __ctx.session.flash and __ctx.session.flash[key] ~= nil then
|
|
return true
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
function session_get_all_flash()
|
|
local flash = {}
|
|
|
|
-- Add session flash data first
|
|
if __ctx.session and __ctx.session.flash then
|
|
for k, v in pairs(__ctx.session.flash) do
|
|
flash[k] = v
|
|
end
|
|
end
|
|
|
|
-- Add current response flash (overwrites session flash if same key)
|
|
if __response.flash then
|
|
for k, v in pairs(__response.flash) do
|
|
flash[k] = v
|
|
end
|
|
end
|
|
|
|
return flash
|
|
end
|
|
|
|
function session_flash_now(key, value)
|
|
-- Flash for current request only (not persisted)
|
|
_G._current_flash = _G._current_flash or {}
|
|
_G._current_flash[key] = value
|
|
end
|
|
|
|
function session_get_flash_now(key)
|
|
return _G._current_flash and _G._current_flash[key]
|
|
end
|
|
|
|
-- ======================================================================
|
|
-- FLASH HELPER FUNCTIONS
|
|
-- ======================================================================
|
|
|
|
function flash_success(message)
|
|
session_flash("success", message)
|
|
end
|
|
|
|
function flash_error(message)
|
|
session_flash("error", message)
|
|
end
|
|
|
|
function flash_warning(message)
|
|
session_flash("warning", message)
|
|
end
|
|
|
|
function flash_info(message)
|
|
session_flash("info", message)
|
|
end
|
|
|
|
function flash_message(type, message)
|
|
session_flash(type, message)
|
|
end
|
|
|
|
-- Get flash messages by type
|
|
function get_flash_success()
|
|
return session_get_flash("success")
|
|
end
|
|
|
|
function get_flash_error()
|
|
return session_get_flash("error")
|
|
end
|
|
|
|
function get_flash_warning()
|
|
return session_get_flash("warning")
|
|
end
|
|
|
|
function get_flash_info()
|
|
return session_get_flash("info")
|
|
end
|
|
|
|
-- Check if flash messages exist
|
|
function has_flash_success()
|
|
return session_has_flash("success")
|
|
end
|
|
|
|
function has_flash_error()
|
|
return session_has_flash("error")
|
|
end
|
|
|
|
function has_flash_warning()
|
|
return session_has_flash("warning")
|
|
end
|
|
|
|
function has_flash_info()
|
|
return session_has_flash("info")
|
|
end
|
|
|
|
-- Convenience function for redirects with flash
|
|
function redirect_with_flash(url, type, message, status)
|
|
session_flash(type or "info", message)
|
|
http_redirect(url, status)
|
|
end
|
|
|
|
function redirect_with_success(url, message, status)
|
|
redirect_with_flash(url, "success", message, status)
|
|
end
|
|
|
|
function redirect_with_error(url, message, status)
|
|
redirect_with_flash(url, "error", message, status)
|
|
end
|