Moonshark/core/runner/sandbox/lua/sandbox.lua
2025-04-07 21:59:11 -05:00

552 lines
15 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
__env_system = {
base_env = {}
}
-- ======================================================================
-- 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
-- Reset session modification flag
__session_modified = false
-- Create environment
local env = __create_env(ctx)
-- Set environment for function
setfenv(fn, env)
-- Execute with protected call
local ok, result = pcall(fn)
if not ok then
error(result, 0)
end
return result
end
-- ======================================================================
-- MODULE LOADING SYSTEM
-- ======================================================================
-- Setup environment-aware require function
function __setup_require(env)
-- Create require function specific to this environment
env.require = function(modname)
-- Check if already loaded
if package.loaded[modname] then
return package.loaded[modname]
end
-- Check preloaded modules
if __ready_modules[modname] then
local loader = package.preload[modname]
if loader then
-- Set environment for loader
setfenv(loader, env)
-- Execute and store result
local result = loader()
if result == nil then
result = true
end
package.loaded[modname] = result
return result
end
end
-- Direct file load as fallback
if __module_paths[modname] then
local path = __module_paths[modname]
local chunk, err = loadfile(path)
if chunk then
setfenv(chunk, env)
local result = chunk()
if result == nil then
result = true
end
package.loaded[modname] = result
return result
end
end
-- Full path search as last resort
local errors = {}
for path in package.path:gmatch("[^;]+") do
local file_path = path:gsub("?", modname:gsub("%.", "/"))
local chunk, err = loadfile(file_path)
if chunk then
setfenv(chunk, env)
local result = chunk()
if result == nil then
result = true
end
package.loaded[modname] = result
return result
end
table.insert(errors, "\tno file '" .. file_path .. "'")
end
error("module '" .. modname .. "' not found:\n" .. table.concat(errors, "\n"), 2)
end
return env
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,
-- 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 {}
local old_options = options
options = {headers = old_options.headers, timeout = old_options.timeout, query = old_options.query}
local response = http.client.request("HEAD", url, nil, options)
return response
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 or legacy params
local opts = {}
if type(options) == "table" then
opts = options
elseif options ~= nil then
-- Legacy support: options is actually 'expires'
opts.expires = options
-- Check for other legacy params (4th-7th args)
local args = {...}
if args[1] then opts.path = args[1] end
if args[2] then opts.domain = args[2] end
if args[3] then opts.secure = args[3] end
if args[4] ~= nil then opts.http_only = args[4] end
end
-- 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
else
-- opts.expires == 0: Session cookie
-- Do nothing (omitting both expires and max-age creates a session cookie)
end
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 and env.ctx.cookies[name] then
return tostring(env.ctx.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
-- ======================================================================
-- Session module implementation
local session = {
-- Get a session value
get = function(key)
if type(key) ~= "string" then
error("session.get: key must be a string", 2)
end
if __session_data and __session_data[key] then
return __session_data[key]
end
return nil
end,
-- Set a session value
set = function(key, value)
if type(key) ~= "string" then
error("session.set: key must be a string", 2)
end
-- Ensure session data table exists
__session_data = __session_data or {}
-- Store value
__session_data[key] = value
-- Mark session as modified
__session_modified = true
return true
end,
-- Delete a session value
delete = function(key)
if type(key) ~= "string" then
error("session.delete: key must be a string", 2)
end
if __session_data then
__session_data[key] = nil
__session_modified = true
end
return true
end,
-- Clear all session data
clear = function()
__session_data = {}
__session_modified = true
return true
end,
-- Get the session ID
get_id = function()
return __session_id or nil
end,
-- Get all session data
get_all = function()
local result = {}
for k, v in pairs(__session_data or {}) do
result[k] = v
end
return result
end,
-- Check if session has a key
has = function(key)
if type(key) ~= "string" then
error("session.has: key must be a string", 2)
end
return __session_data and __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 = util.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.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
-- This is safe since Lua strings are immutable
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
}
-- ======================================================================
-- REGISTER MODULES GLOBALLY
-- ======================================================================
-- Install modules in global scope
_G.http = http
_G.cookie = cookie
_G.session = session
_G.csrf = csrf
-- Register modules in sandbox base environment
__env_system.base_env.http = http
__env_system.base_env.cookie = cookie
__env_system.base_env.session = session
__env_system.base_env.csrf = csrf