Moonshark/core/runner/sandbox.lua
2025-04-09 23:12:23 -05:00

584 lines
16 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.
]]--
-- Global tables for execution context
__http_responses = {}
__module_paths = {}
__module_bytecode = {}
__ready_modules = {}
__session_data = {}
__session_id = nil
__session_modified = false
-- ======================================================================
-- CORE SANDBOX FUNCTIONALITY
-- ======================================================================
-- Create environment inheriting from _G
function __create_env(ctx)
-- Create environment with metatable inheriting from _G
local env = setmetatable({}, {__index = _G})
-- Add context if provided
if ctx then
env.ctx = ctx
end
-- Add proper require function to this environment
if __setup_require then
__setup_require(env)
end
return env
end
-- Execute script with clean environment
function __execute_script(fn, ctx)
-- Clear previous responses
__http_responses[1] = nil
-- Create environment with metatable inheriting from _G
local env = setmetatable({}, {__index = _G})
-- Add context if provided
if ctx then
env.ctx = ctx
end
print("INIT SESSION DATA:", util.json_encode(ctx.session_data or {}))
-- Initialize local session variables in the environment
env.__session_data = ctx.session_data or {}
env.__session_id = ctx.session_id
env.__session_modified = false
-- Add proper require function to this environment
if __setup_require then
__setup_require(env)
end
-- Set environment for function
setfenv(fn, env)
-- Execute with protected call
local ok, result = pcall(fn)
if not ok then
error(result, 0)
end
-- If session was modified, add to response
if env.__session_modified then
__http_responses[1] = __http_responses[1] or {}
__http_responses[1].session_data = env.__session_data
__http_responses[1].session_id = env.__session_id
__http_responses[1].session_modified = true
end
print("SESSION MODIFIED:", env.__session_modified)
print("FINAL DATA:", util.json_encode(env.__session_data or {}))
return result
end
-- ======================================================================
-- HTTP MODULE
-- ======================================================================
-- HTTP module implementation
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 = __http_responses[1] or {}
resp.status = code
__http_responses[1] = resp
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 = __http_responses[1] or {}
resp.headers = resp.headers or {}
resp.headers[name] = value
__http_responses[1] = resp
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 = __http_responses[1] or {}
resp.metadata = resp.metadata or {}
resp.metadata[key] = value
__http_responses[1] = resp
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,
-- Simple GET request
get = function(url, options)
return http.client.request("GET", url, nil, options)
end,
-- Simple POST request with automatic content-type
post = function(url, body, options)
options = options or {}
return http.client.request("POST", url, body, options)
end,
-- Simple PUT request with automatic content-type
put = function(url, body, options)
options = options or {}
return http.client.request("PUT", url, body, options)
end,
-- Simple DELETE request
delete = function(url, options)
return http.client.request("DELETE", url, nil, options)
end,
-- Simple PATCH request
patch = function(url, body, options)
options = options or {}
return http.client.request("PATCH", url, body, options)
end,
-- Simple HEAD request
head = function(url, options)
options = options or {}
return http.client.request("HEAD", url, nil, options)
end,
-- Simple OPTIONS request
options = function(url, options)
return http.client.request("OPTIONS", url, nil, options)
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
}
}
-- ======================================================================
-- COOKIE MODULE
-- ======================================================================
-- Cookie module implementation
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
-- Get or create response
local resp = __http_responses[1] or {}
resp.cookies = resp.cookies or {}
__http_responses[1] = resp
-- Handle options as table
local opts = options or {}
-- Create cookie table
local cookie = {
name = name,
value = value or "",
path = opts.path or "/",
domain = opts.domain
}
-- Handle expiry
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
-- Security flags
cookie.secure = (opts.secure ~= false)
cookie.http_only = (opts.http_only ~= false)
-- Store in cookies table
local n = #resp.cookies + 1
resp.cookies[n] = 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
-- Access values directly from current environment
local env = getfenv(2)
-- Check if context exists and has cookies
if env.ctx and env.ctx.cookies then
return env.ctx.cookies[name]
end
-- If context has request_cookies map
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
-- Create an expired cookie
return cookie.set(name, "", {expires = 0, path = path or "/", domain = domain})
end
}
-- ======================================================================
-- SESSION MODULE
-- ======================================================================
local session = {
-- Get session value
get = function(key)
if type(key) ~= "string" then
error("session.get: key must be a string", 2)
end
local env = getfenv(2)
return env.__session_data and env.__session_data[key]
end,
-- Set session value
set = function(key, value)
if type(key) ~= "string" then
error("session.set: key must be a string", 2)
end
local env = getfenv(2)
print("SET ENV:", tostring(env)) -- Debug the environment
if not env.__session_data then
env.__session_data = {}
print("CREATED NEW SESSION TABLE")
end
env.__session_data[key] = value
env.__session_modified = true
print("SET:", key, "=", tostring(value), "MODIFIED:", env.__session_modified)
return true
end,
-- Delete session value
delete = function(key)
if type(key) ~= "string" then
error("session.delete: key must be a string", 2)
end
local env = getfenv(2)
if env.__session_data and env.__session_data[key] ~= nil then
env.__session_data[key] = nil
env.__session_modified = true
end
return true
end,
-- Clear all session data
clear = function()
local env = getfenv(2)
if env.__session_data and next(env.__session_data) then
env.__session_data = {}
env.__session_modified = true
end
return true
end,
-- Get session ID
get_id = function()
local env = getfenv(2)
return env.__session_id or ""
end,
-- Get all session data
get_all = function()
local env = getfenv(2)
return env.__session_data or {}
end,
-- Check if session has key
has = function(key)
if type(key) ~= "string" then
error("session.has: key must be a string", 2)
end
local env = getfenv(2)
return env.__session_data ~= nil and env.__session_data[key] ~= nil
end
}
-- ======================================================================
-- CSRF MODULE
-- ======================================================================
-- CSRF protection module
local csrf = {
-- Session key where the token is stored
TOKEN_KEY = "_csrf_token",
-- Default form field name
DEFAULT_FIELD = "csrf",
-- Generate a new CSRF token and store it in the session
generate = function(length)
-- Default length is 32 characters
length = length or 32
if length < 16 then
-- Enforce minimum security
length = 16
end
-- Check if we have a session module
if not session then
error("CSRF protection requires the session module", 2)
end
local token = __generate_token(length)
session.set(csrf.TOKEN_KEY, token)
return token
end,
-- Get the current token or generate a new one
token = function()
-- Get from session if exists
local token = session.get(csrf.TOKEN_KEY)
-- Generate if needed
if not token then
token = csrf.generate()
end
return token
end,
-- Generate a hidden form field with the CSRF token
field = function(field_name)
field_name = field_name or csrf.DEFAULT_FIELD
local token = csrf.token()
return string.format('<input type="hidden" name="%s" value="%s">', field_name, token)
end,
-- Verify a given token against the session token
verify = function(token, field_name)
field_name = field_name or csrf.DEFAULT_FIELD
local env = getfenv(2)
local form = nil
if env.ctx and env.ctx._request_form then
form = env.ctx._request_form
elseif env.ctx and env.ctx.form then
form = env.ctx.form
else
return false
end
token = token or form[field_name]
if not token then
return false
end
local session_token = session.get(csrf.TOKEN_KEY)
if not session_token then
return false
end
-- Constant-time comparison to prevent timing attacks
if #token ~= #session_token then
return false
end
local result = true
for i = 1, #token do
if token:sub(i, i) ~= session_token:sub(i, i) then
result = false
-- Don't break early - continue to prevent timing attacks
end
end
return result
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
-- ======================================================================
-- Install modules in global scope
_G.http = http
_G.cookie = cookie
_G.session = session
_G.csrf = csrf
_G.util = util