399 lines
10 KiB
Lua
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
|