--[[ 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('', 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