584 lines
16 KiB
Lua
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
|