Moonshark/core/runner/sandbox.lua
2025-04-10 13:01:17 -05:00

399 lines
10 KiB
Lua

--[[
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 = {}
-- ======================================================================
-- CORE SANDBOX FUNCTIONALITY
-- ======================================================================
-- 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)
setfenv(fn, env)
local ok, result = pcall(fn)
if not ok then
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
}
-- ======================================================================
-- UTIL MODULE
-- ======================================================================
-- Utility module implementation
local util = {
-- Generate a token (wrapper around __generate_token)
generate_token = function(length)
return __generate_token(length or 32)
end,
-- Simple JSON stringify (for when you just need a quick string)
json_encode = function(value)
if type(value) == "table" then
local json = "{"
local sep = ""
for k, v in pairs(value) do
json = json .. sep
if type(k) == "number" then
-- Array-like
json = json .. util.json_encode(v)
else
-- Object-like
json = json .. '"' .. k .. '":' .. util.json_encode(v)
end
sep = ","
end
return json .. "}"
elseif type(value) == "string" then
return '"' .. value:gsub('"', '\\"'):gsub('\n', '\\n') .. '"'
elseif type(value) == "number" then
return tostring(value)
elseif type(value) == "boolean" then
return value and "true" or "false"
elseif value == nil then
return "null"
end
return '"' .. tostring(value) .. '"'
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
}
}
-- ======================================================================
-- REGISTER MODULES GLOBALLY
-- ======================================================================
_G.http = http
_G.session = session
_G.cookie = cookie
_G.util = util