--[[ 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 __session_modified_keys = {} -- ====================================================================== -- 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 -- ====================================================================== -- 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 -- ====================================================================== -- 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 return __session_data and __session_data[key] end, -- Set a session value set = function(key, value) if type(key) ~= "string" then error("session.set: key must be a string", 2) end __session_data = __session_data or {} __session_data[key] = value __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 and __session_data[key] ~= nil then __session_data[key] = nil __session_modified = true __session_modified_keys[key] = true end return true end, -- Clear all session data clear = function() if __session_data and next(__session_data) then -- Track all keys as modified for k in pairs(__session_data) do __session_modified_keys[k] = true end __session_data = {} __session_modified = true end 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 = __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('', 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