552 lines
15 KiB
Lua
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 |