From 22c340648b3aa96683a2bce10348f3935e4240dd Mon Sep 17 00:00:00 2001 From: Sky Johnson Date: Tue, 3 Jun 2025 18:34:22 -0500 Subject: [PATCH] massive optimizations and fixes --- go.mod | 2 +- go.sum | 4 +- http/server.go | 2 +- runner/lua/env.lua | 35 +-- runner/lua/sandbox.lua | 612 ++++++++++++++++++++++++--------------- runner/lualibs/http.go | 15 +- runner/lualibs/sqlite.go | 89 ++---- runner/lualibs/util.go | 2 +- runner/sandbox.go | 129 +++++---- 9 files changed, 497 insertions(+), 393 deletions(-) diff --git a/go.mod b/go.mod index f4165c4..7dde99f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.24.1 require ( git.sharkk.net/Go/LRU v1.0.0 git.sharkk.net/Sharkk/Fin v1.2.0 - git.sharkk.net/Sky/LuaJIT-to-Go v0.5.1 + git.sharkk.net/Sky/LuaJIT-to-Go v0.5.2 github.com/VictoriaMetrics/fastcache v1.12.4 github.com/alexedwards/argon2id v1.0.0 github.com/deneonet/benc v1.1.8 diff --git a/go.sum b/go.sum index af87860..f0efd6e 100644 --- a/go.sum +++ b/go.sum @@ -2,8 +2,8 @@ git.sharkk.net/Go/LRU v1.0.0 h1:/KqdRVhHldi23aVfQZ4ss6vhCWZqA3vFiQyf1MJPpQc= git.sharkk.net/Go/LRU v1.0.0/go.mod h1:8tdTyl85mss9a+KKwo+Wj9gKHOizhfLfpJhz1ltYz50= git.sharkk.net/Sharkk/Fin v1.2.0 h1:axhme8vHRYoaB3us7PNfXzXxKOxhpS5BMuNpN8ESe6U= git.sharkk.net/Sharkk/Fin v1.2.0/go.mod h1:ca0Ej9yCM/vHh1o3YMvBZspme3EtbwoEL2UXN5UPXMo= -git.sharkk.net/Sky/LuaJIT-to-Go v0.5.1 h1:e9rby0xJs8m2SAPv0di/LplDok88UyjcNjKu8S4d1BY= -git.sharkk.net/Sky/LuaJIT-to-Go v0.5.1/go.mod h1:HQz+D7AFxOfNbTIogjxP+shEBtz1KKrLlLucU+w07c8= +git.sharkk.net/Sky/LuaJIT-to-Go v0.5.2 h1:BgsZPkoqJjQ7Rb+bSs7QQ24+wwLzyc2AALbnpB/n9Kw= +git.sharkk.net/Sky/LuaJIT-to-Go v0.5.2/go.mod h1:HQz+D7AFxOfNbTIogjxP+shEBtz1KKrLlLucU+w07c8= github.com/VictoriaMetrics/fastcache v1.12.4 h1:2xvmwZBW+9QtHsXggfzAZRs1FZWCsBs8QDg22bMidf0= github.com/VictoriaMetrics/fastcache v1.12.4/go.mod h1:K+JGPBn0sueFlLjZ8rcVM0cKkWKNElKyQXmw57QOoYI= github.com/alexedwards/argon2id v1.0.0 h1:wJzDx66hqWX7siL/SRUmgz3F8YMrd/nfX/xHHcQQP0w= diff --git a/http/server.go b/http/server.go index 465c27e..a647116 100644 --- a/http/server.go +++ b/http/server.go @@ -257,7 +257,7 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip } for k, v := range response.SessionData { - if v == "__SESSION_DELETE_MARKER__" { + if v == "__DELETE__" { session.Delete(k) } else { session.Set(k, v) diff --git a/runner/lua/env.lua b/runner/lua/env.lua index b3b77a5..068d207 100644 --- a/runner/lua/env.lua +++ b/runner/lua/env.lua @@ -2,44 +2,41 @@ -- Provides access to persistent environment variables stored in .env file -- Get an environment variable with a default value --- Returns the value if it exists, default_value otherwise function env_get(key, default_value) if type(key) ~= "string" then error("env_get: key must be a string") end - -- First check context for environment variables (no Go call needed) - if _env and _env[key] ~= nil then - return _env[key] + -- Check context for environment variables + if __ctx and __ctx._env and __ctx._env[key] ~= nil then + return __ctx._env[key] end return default_value end -- Set an environment variable --- Returns true on success, false on failure function env_set(key, value) if type(key) ~= "string" then error("env_set: key must be a string") end -- Update context immediately for future reads - if not _env then - _env = {} + if __ctx then + __ctx._env = __ctx._env or {} + __ctx._env[key] = value end - _env[key] = value -- Persist to Go backend return __env_set(key, value) end -- Get all environment variables as a table --- Returns a table with all key-value pairs function env_get_all() -- Return context table directly if available - if _env then + if __ctx and __ctx._env then local copy = {} - for k, v in pairs(_env) do + for k, v in pairs(__ctx._env) do copy[k] = v end return copy @@ -50,36 +47,36 @@ function env_get_all() end -- Check if an environment variable exists --- Returns true if the variable exists, false otherwise function env_exists(key) if type(key) ~= "string" then error("env_exists: key must be a string") end -- Check context first - if _env then - return _env[key] ~= nil + if __ctx and __ctx._env then + return __ctx._env[key] ~= nil end return false end -- Set multiple environment variables from a table --- Returns true on success, false if any setting failed function env_set_many(vars) if type(vars) ~= "table" then error("env_set_many: vars must be a table") end - if not _env then - _env = {} + if __ctx then + __ctx._env = __ctx._env or {} end local success = true for key, value in pairs(vars) do if type(key) == "string" and type(value) == "string" then -- Update context - _env[key] = value + if __ctx and __ctx._env then + __ctx._env[key] = value + end -- Persist to Go if not __env_set(key, value) then success = false @@ -90,4 +87,4 @@ function env_set_many(vars) end return success -end +end \ No newline at end of file diff --git a/runner/lua/sandbox.lua b/runner/lua/sandbox.lua index 4017a71..ea17792 100644 --- a/runner/lua/sandbox.lua +++ b/runner/lua/sandbox.lua @@ -1,244 +1,390 @@ --- Simplified sandbox.lua - Direct execution with explicit state passing +-- Simplified sandbox.lua - Global function approach --- Main execution wrapper - receives script, context, and response object function __execute(script_func, ctx, response) - -- Create clean environment - local env = setmetatable({}, {__index = _G}) - - -- Direct context and response access - env.ctx = ctx - env.response = response - - -- Exit sentinel - env.exit = function() error("__EXIT__") end - - -- ====================================================================== - -- HTTP FUNCTIONS - Modify response directly - -- ====================================================================== - - env.http_set_status = function(code) - if type(code) ~= "number" then - error("http_set_status: status code must be a number", 2) - end - response.status = code - end - - env.http_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 - response.headers = response.headers or {} - response.headers[name] = value - end - - env.http_set_content_type = function(ct) - env.http_set_header("Content-Type", ct) - end - - env.http_set_metadata = function(key, value) - if type(key) ~= "string" then - error("http_set_metadata: key must be a string", 2) - end - response.metadata = response.metadata or {} - response.metadata[key] = value - end - - env.http_redirect = function(url, status) - if type(url) ~= "string" then - error("http_redirect: url must be a string", 2) - end - response.status = status or 302 - response.headers = response.headers or {} - response.headers["Location"] = url - env.exit() - end - - -- HTTP request functions (use native implementation) - env.http_request = __http_request - - local function make_method(method, needs_body) - return function(url, body_or_options, options) - if needs_body then - options = options or {} - return env.http_request(method, url, body_or_options, options) - else - body_or_options = body_or_options or {} - return env.http_request(method, url, nil, body_or_options) - end - end - end - - env.http_get = make_method("GET", false) - env.http_post = make_method("POST", true) - env.http_put = make_method("PUT", true) - env.http_patch = make_method("PATCH", true) - env.http_delete = make_method("DELETE", false) - env.http_head = make_method("HEAD", false) - env.http_options = make_method("OPTIONS", false) - - -- ====================================================================== - -- COOKIE FUNCTIONS - Direct access to ctx and response - -- ====================================================================== - - env.cookie_set = function(name, value, options) - if type(name) ~= "string" then - error("cookie_set: name must be a string", 2) - end - - response.cookies = response.cookies or {} - local opts = options or {} - - local cookie = { - name = name, - value = value or "", - path = opts.path or "/", - domain = opts.domain, - secure = opts.secure ~= false, - http_only = opts.http_only ~= false, - same_site = opts.same_site or "Lax" - } - - if opts.expires then - if type(opts.expires) == "number" and opts.expires > 0 then - cookie.max_age = opts.expires - end - end - - table.insert(response.cookies, cookie) - return true - end - - env.cookie_get = function(name) - if type(name) ~= "string" then - error("cookie_get: name must be a string", 2) - end - return ctx.cookies and ctx.cookies[name] - end - - env.cookie_remove = function(name, path, domain) - return env.cookie_set(name, "", {expires = -1, path = path or "/", domain = domain}) - end - - -- ====================================================================== - -- SESSION FUNCTIONS - Direct access to ctx and response - -- ====================================================================== - - env.session_get = function(key) - if type(key) ~= "string" then - error("session_get: key must be a string", 2) - end - return ctx.session and ctx.session.data and ctx.session.data[key] - end - - env.session_set = function(key, value) - if type(key) ~= "string" then - error("session_set: key must be a string", 2) - end - response.session = response.session or {} - response.session[key] = value - -- Update context if available - if ctx.session and ctx.session.data then - ctx.session.data[key] = value - end - end - - env.session_id = function() - return ctx.session and ctx.session.id - end - - env.session_get_all = function() - if ctx.session and ctx.session.data then - local copy = {} - for k, v in pairs(ctx.session.data) do - copy[k] = v - end - return copy - end - return {} - end - - env.session_delete = function(key) - if type(key) ~= "string" then - error("session_delete: key must be a string", 2) - end - response.session = response.session or {} - response.session[key] = "__DELETE__" - if ctx.session and ctx.session.data then - ctx.session.data[key] = nil - end - end - - env.session_clear = function() - response.session = {__clear_all = true} - if ctx.session and ctx.session.data then - for k in pairs(ctx.session.data) do - ctx.session.data[k] = nil - end - end - end - - -- ====================================================================== - -- CONTENT TYPE HELPERS - -- ====================================================================== - - env.send_html = function(content) - env.http_set_content_type("text/html") - return content - end - - env.send_json = function(content) - env.http_set_content_type("application/json") - return content - end - - env.send_text = function(content) - env.http_set_content_type("text/plain") - return content - end - - -- ====================================================================== - -- NATIVE FUNCTIONS (injected by Go) - -- ====================================================================== - - -- Copy over native functions - local natives = { - "__password_hash", "__password_verify", - "__sqlite_open", "__sqlite_exec", "__sqlite_query", "__sqlite_close", - "__fs_read", "__fs_write", "__fs_exists", "__fs_delete", - "generate_token", "url_encode", "url_decode", - "html_special_chars", "html_entities", - "base64_encode", "base64_decode", - "json_encode", "json_decode", - "sha256", "md5", "hmac", - "env_get", "env_set" - } - - for _, name in ipairs(natives) do - if _G[name] then - env[name:gsub("^__", "")] = _G[name] - end - end - - -- Template functions - env.render = _G.render - env.parse = _G.parse - env.iparse = _G.iparse - - -- Module support - if __setup_require then - __setup_require(env) - end - - -- Set function environment and execute - setfenv(script_func, env) + -- Store context and response globally for function access + __ctx = ctx + __response = response + -- Execute script in global environment local ok, result = pcall(script_func) + + -- Clean up + __ctx = nil + __response = nil + if not ok then if result == "__EXIT__" then - return nil + return {nil, response} end error(result, 0) end - return result + return {result, response} +end + +-- Exit sentinel +function exit() + error("__EXIT__") +end + +-- ====================================================================== +-- HTTP RESPONSE FUNCTIONS +-- ====================================================================== + +function http_set_status(code) + __response.status = code +end + +function http_set_header(name, value) + __response.headers = __response.headers or {} + __response.headers[name] = value +end + +function http_set_content_type(ct) + __response.headers = __response.headers or {} + __response.headers["Content-Type"] = ct +end + +function http_set_metadata(key, value) + __response.metadata = __response.metadata or {} + __response.metadata[key] = value +end + +function http_redirect(url, status) + __response.status = status or 302 + __response.headers = __response.headers or {} + __response.headers["Location"] = url + error("__EXIT__") +end + +-- ====================================================================== +-- COOKIE FUNCTIONS +-- ====================================================================== + +function cookie_set(name, value, options) + __response.cookies = __response.cookies or {} + local opts = options or {} + local cookie = { + name = name, + value = value or "", + path = opts.path or "/", + domain = opts.domain, + secure = opts.secure ~= false, + http_only = opts.http_only ~= false + } + if opts.expires and opts.expires > 0 then + cookie.max_age = opts.expires + end + table.insert(__response.cookies, cookie) +end + +function cookie_get(name) + return __ctx._request_cookies and __ctx._request_cookies[name] +end + +function cookie_remove(name, path, domain) + return cookie_set(name, "", {expires = -1, path = path or "/", domain = domain}) +end + +-- ====================================================================== +-- SESSION FUNCTIONS +-- ====================================================================== + +function session_set(key, value) + __response.session = __response.session or {} + __response.session[key] = value + if __ctx.session and __ctx.session.data then + __ctx.session.data[key] = value + end +end + +function session_get(key) + return __ctx.session and __ctx.session.data and __ctx.session.data[key] +end + +function session_id() + return __ctx.session and __ctx.session.id +end + +function session_get_all() + if __ctx.session and __ctx.session.data then + local copy = {} + for k, v in pairs(__ctx.session.data) do + copy[k] = v + end + return copy + end + return {} +end + +function session_delete(key) + __response.session = __response.session or {} + __response.session[key] = "__DELETE__" + if __ctx.session and __ctx.session.data then + __ctx.session.data[key] = nil + end +end + +function session_clear() + __response.session = {__clear_all = true} + if __ctx.session and __ctx.session.data then + for k in pairs(__ctx.session.data) do + __ctx.session.data[k] = nil + end + end +end + +-- ====================================================================== +-- CSRF FUNCTIONS +-- ====================================================================== + +function csrf_generate() + local token = generate_token(32) + session_set("_csrf_token", token) + return token +end + +function csrf_field() + local token = session_get("_csrf_token") + if not token then + token = csrf_generate() + end + return string.format('', + html_special_chars(token)) +end + +function csrf_validate() + local token = __ctx.session and __ctx.session.data and __ctx.session.data["_csrf_token"] + if not token then + __response.status = 403 + error("__EXIT__") + end + + local request_token = (__ctx._request_form and __ctx._request_form._csrf_token) or + (__ctx._request_headers and (__ctx._request_headers["x-csrf-token"] or __ctx._request_headers["csrf-token"])) + + if not request_token or request_token ~= token then + __response.status = 403 + error("__EXIT__") + end + return true +end + +-- ====================================================================== +-- CONTENT TYPE HELPERS +-- ====================================================================== + +function send_html(content) + http_set_content_type("text/html") + return content +end + +function send_json(content) + http_set_content_type("application/json") + return content +end + +function send_text(content) + http_set_content_type("text/plain") + return content +end + +function send_xml(content) + http_set_content_type("application/xml") + return content +end + +function send_javascript(content) + http_set_content_type("application/javascript") + return content +end + +function send_css(content) + http_set_content_type("text/css") + return content +end + +function send_svg(content) + http_set_content_type("image/svg+xml") + return content +end + +function send_csv(content) + http_set_content_type("text/csv") + return content +end + +function send_binary(content, mime_type) + http_set_content_type(mime_type or "application/octet-stream") + return content +end + +-- ====================================================================== +-- TEMPLATE RENDER FUNCTIONS +-- ====================================================================== + +-- Template processing with code execution +function render(template_str, env) + local function get_line(s, ln) + for line in s:gmatch("([^\n]*)\n?") do + if ln == 1 then return line end + ln = ln - 1 + end + end + + local function pos_to_line(s, pos) + local line = 1 + for _ in s:sub(1, pos):gmatch("\n") do line = line + 1 end + return line + end + + local pos, chunks = 1, {} + while pos <= #template_str do + local unescaped_start = template_str:find("{{{", pos, true) + local escaped_start = template_str:find("{{", pos, true) + + local start, tag_type, open_len + if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then + start, tag_type, open_len = unescaped_start, "-", 3 + elseif escaped_start then + start, tag_type, open_len = escaped_start, "=", 2 + else + table.insert(chunks, template_str:sub(pos)) + break + end + + if start > pos then + table.insert(chunks, template_str:sub(pos, start-1)) + end + + pos = start + open_len + local close_tag = tag_type == "-" and "}}}" or "}}" + local close_start, close_stop = template_str:find(close_tag, pos, true) + if not close_start then + error("Failed to find closing tag at position " .. pos) + end + + local code = template_str:sub(pos, close_start-1):match("^%s*(.-)%s*$") + + -- Check if it's a simple variable name for escaped output + local is_simple_var = tag_type == "=" and code:match("^[%w_]+$") + + table.insert(chunks, {tag_type, code, pos, is_simple_var}) + pos = close_stop + 1 + end + + local buffer = {"local _tostring, _escape, _b, _b_i = ...\n"} + for _, chunk in ipairs(chunks) do + local t = type(chunk) + if t == "string" then + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "_b[_b_i] = " .. string.format("%q", chunk) .. "\n") + else + t = chunk[1] + if t == "=" then + if chunk[4] then -- is_simple_var + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = _escape(_tostring(" .. chunk[2] .. "))\n") + else + table.insert(buffer, "--[[" .. chunk[3] .. "]] " .. chunk[2] .. "\n") + end + elseif t == "-" then + table.insert(buffer, "_b_i = _b_i + 1\n") + table.insert(buffer, "--[[" .. chunk[3] .. "]] _b[_b_i] = _tostring(" .. chunk[2] .. ")\n") + end + end + end + table.insert(buffer, "return _b") + + local fn, err = loadstring(table.concat(buffer)) + if not fn then error(err) end + + env = env or {} + local runtime_env = setmetatable({}, {__index = function(_, k) return env[k] or _G[k] end}) + setfenv(fn, runtime_env) + + local output_buffer = {} + fn(tostring, html_special_chars, output_buffer, 0) + return table.concat(output_buffer) +end + +-- Named placeholder processing +function parse(template_str, env) + local pos, output = 1, {} + env = env or {} + + while pos <= #template_str do + local unescaped_start, unescaped_end, unescaped_name = template_str:find("{{{%s*([%w_]+)%s*}}}", pos) + local escaped_start, escaped_end, escaped_name = template_str:find("{{%s*([%w_]+)%s*}}", pos) + + local next_pos, placeholder_end, name, escaped + if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then + next_pos, placeholder_end, name, escaped = unescaped_start, unescaped_end, unescaped_name, false + elseif escaped_start then + next_pos, placeholder_end, name, escaped = escaped_start, escaped_end, escaped_name, true + else + local text = template_str:sub(pos) + if text and #text > 0 then + table.insert(output, text) + end + break + end + + local text = template_str:sub(pos, next_pos - 1) + if text and #text > 0 then + table.insert(output, text) + end + + local value = env[name] + local str = tostring(value or "") + if escaped then + str = html_special_chars(str) + end + table.insert(output, str) + + pos = placeholder_end + 1 + end + + return table.concat(output) +end + +-- Indexed placeholder processing +function iparse(template_str, values) + local pos, output, value_index = 1, {}, 1 + values = values or {} + + while pos <= #template_str do + local unescaped_start, unescaped_end = template_str:find("{{{}}}", pos, true) + local escaped_start, escaped_end = template_str:find("{{}}", pos, true) + + local next_pos, placeholder_end, escaped + if unescaped_start and (not escaped_start or unescaped_start <= escaped_start) then + next_pos, placeholder_end, escaped = unescaped_start, unescaped_end, false + elseif escaped_start then + next_pos, placeholder_end, escaped = escaped_start, escaped_end, true + else + local text = template_str:sub(pos) + if text and #text > 0 then + table.insert(output, text) + end + break + end + + local text = template_str:sub(pos, next_pos - 1) + if text and #text > 0 then + table.insert(output, text) + end + + local value = values[value_index] + local str = tostring(value or "") + if escaped then + str = html_special_chars(str) + end + table.insert(output, str) + + pos = placeholder_end + 1 + value_index = value_index + 1 + end + + return table.concat(output) end \ No newline at end of file diff --git a/runner/lualibs/http.go b/runner/lualibs/http.go index a7302a0..88d6083 100644 --- a/runner/lualibs/http.go +++ b/runner/lualibs/http.go @@ -100,7 +100,7 @@ func httpRequest(state *luajit.State) int { req.SetBodyString(bodyStr) } else if state.IsTable(3) { // Table body - convert to JSON - luaTable, err := state.SafeToTable(3) + luaTable, err := state.ToTable(3) if err != nil { return state.PushError("Failed to parse body table: %v", err) } @@ -123,14 +123,9 @@ func httpRequest(state *luajit.State) int { // Process options (headers, timeout, etc.) timeout := config.DefaultTimeout if state.GetTop() >= 4 && !state.IsNil(4) && state.IsTable(4) { - // Process headers using ForEachTableKV if headers, ok := state.GetFieldTable(4, "headers"); ok { - if headerMap, ok := headers.(map[string]string); ok { + if headerMap, ok := headers.(map[string]any); ok { for name, value := range headerMap { - req.Header.Set(name, value) - } - } else if headerMapAny, ok := headers.(map[string]any); ok { - for name, value := range headerMapAny { if valueStr, ok := value.(string); ok { req.Header.Set(name, valueStr) } @@ -154,12 +149,8 @@ func httpRequest(state *luajit.State) int { if query, ok := state.GetFieldTable(4, "query"); ok { args := req.URI().QueryArgs() - if queryMap, ok := query.(map[string]string); ok { + if queryMap, ok := query.(map[string]any); ok { for name, value := range queryMap { - args.Add(name, value) - } - } else if queryMapAny, ok := query.(map[string]any); ok { - for name, value := range queryMapAny { switch v := value.(type) { case string: args.Add(name, v) diff --git a/runner/lualibs/sqlite.go b/runner/lualibs/sqlite.go index d8d8a41..9de43d1 100644 --- a/runner/lualibs/sqlite.go +++ b/runner/lualibs/sqlite.go @@ -239,36 +239,29 @@ func sqlExec(state *luajit.State) int { func setupParams(state *luajit.State, paramIndex int, execOpts *sqlitex.ExecOptions) error { if state.IsTable(paramIndex) { - paramsAny, err := state.SafeToTable(paramIndex) + paramsAny, err := state.ToTable(paramIndex) if err != nil { return fmt.Errorf("invalid parameters: %w", err) } - switch params := paramsAny.(type) { - case map[string]any: - if arr, ok := params[""]; ok { - if arrParams, ok := arr.([]any); ok { - execOpts.Args = arrParams - } else if floatArr, ok := arr.([]float64); ok { - args := make([]any, len(floatArr)) - for i, v := range floatArr { - args[i] = v - } - execOpts.Args = args - } - } else { - named := make(map[string]any, len(params)) - for k, v := range params { - if len(k) > 0 && k[0] != ':' { - named[":"+k] = v - } else { - named[k] = v - } - } - execOpts.Named = named - } + params, ok := paramsAny.(map[string]any) + if !ok { + return fmt.Errorf("unsupported parameter type: %T", paramsAny) + } - case map[string]string: + // Check for array-style parameters (empty string key indicates array) + if arr, ok := params[""]; ok { + if arrParams, ok := arr.([]any); ok { + execOpts.Args = arrParams + } else if floatArr, ok := arr.([]float64); ok { + args := make([]any, len(floatArr)) + for i, v := range floatArr { + args[i] = v + } + execOpts.Args = args + } + } else { + // Named parameters named := make(map[string]any, len(params)) for k, v := range params { if len(k) > 0 && k[0] != ':' { @@ -278,53 +271,9 @@ func setupParams(state *luajit.State, paramIndex int, execOpts *sqlitex.ExecOpti } } execOpts.Named = named - - case map[string]int: - named := make(map[string]any, len(params)) - for k, v := range params { - if len(k) > 0 && k[0] != ':' { - named[":"+k] = v - } else { - named[k] = v - } - } - execOpts.Named = named - - case map[int]any: - named := make(map[string]any, len(params)) - for k, v := range params { - named[fmt.Sprintf(":%d", k)] = v - } - execOpts.Named = named - - case []any: - execOpts.Args = params - - case []string: - args := make([]any, len(params)) - for i, v := range params { - args[i] = v - } - execOpts.Args = args - - case []int: - args := make([]any, len(params)) - for i, v := range params { - args[i] = v - } - execOpts.Args = args - - case []float64: - args := make([]any, len(params)) - for i, v := range params { - args[i] = v - } - execOpts.Args = args - - default: - return fmt.Errorf("unsupported parameter type: %T", params) } } else { + // Multiple individual parameters count := state.GetTop() - 2 args := make([]any, count) for i := range count { diff --git a/runner/lualibs/util.go b/runner/lualibs/util.go index 243f247..5cf45fa 100644 --- a/runner/lualibs/util.go +++ b/runner/lualibs/util.go @@ -150,7 +150,7 @@ func jsonMarshal(state *luajit.State) int { return state.PushError("json marshal: %v", err) } - value, err := state.SafeToTable(1) + value, err := state.ToTable(1) if err != nil { // Try as generic value if not a table value, err = state.ToValue(1) diff --git a/runner/sandbox.go b/runner/sandbox.go index 04d0a1a..e48762c 100644 --- a/runner/sandbox.go +++ b/runner/sandbox.go @@ -1,8 +1,11 @@ package runner import ( + "Moonshark/runner/lualibs" "fmt" + "maps" + luajit "git.sharkk.net/Sky/LuaJIT-to-Go" "github.com/valyala/fasthttp" ) @@ -42,7 +45,23 @@ func (s *Sandbox) Setup(state *luajit.State, verbose bool) error { // Execute runs a Lua script in the sandbox with the given context func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (*Response, error) { - // Create response object in Lua + // Load script and executor + if err := state.LoadBytecode(bytecode, "script"); err != nil { + return nil, fmt.Errorf("failed to load bytecode: %w", err) + } + + if err := state.LoadBytecode(s.executorBytecode, "executor"); err != nil { + state.Pop(1) + return nil, fmt.Errorf("failed to load executor: %w", err) + } + + // Get __execute function + if err := state.Call(0, 1); err != nil { + state.Pop(1) + return nil, fmt.Errorf("failed to get executor: %w", err) + } + + // Prepare response object response := map[string]any{ "status": 200, "headers": make(map[string]string), @@ -51,52 +70,36 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx *Context) (* "session": make(map[string]any), } - // Load script bytecode (pushes function) - if err := state.LoadBytecode(bytecode, "script"); err != nil { - return nil, fmt.Errorf("failed to load bytecode: %w", err) - } - - // Load executor (pushes __execute function) - if err := state.LoadBytecode(s.executorBytecode, "executor"); err != nil { - state.Pop(1) // Remove script function - return nil, fmt.Errorf("failed to load executor: %w", err) - } - - // Call the loaded executor to get __execute - if err := state.Call(0, 1); err != nil { - state.Pop(1) // Remove script function - return nil, fmt.Errorf("failed to get executor: %w", err) - } - - // Stack: [script_func, __execute] - state.PushCopy(-2) // Copy script function - // Stack: [script_func, __execute, script_func] - - // Push context - if err := state.PushValue(ctx.Values); err != nil { - state.Pop(3) - return nil, fmt.Errorf("failed to push context: %w", err) - } - - // Push response object - if err := state.PushValue(response); err != nil { - state.Pop(4) - return nil, fmt.Errorf("failed to push response: %w", err) - } - - // Stack: [script_func, __execute, script_func, ctx, response] // Call __execute(script_func, ctx, response) + state.PushCopy(-2) // script function + state.PushValue(ctx.Values) + state.PushValue(response) + if err := state.Call(3, 1); err != nil { - state.Pop(1) // Clean up + state.Pop(1) return nil, fmt.Errorf("script execution failed: %w", err) } - // Get the result + // Extract result result, _ := state.ToValue(-1) - state.Pop(2) // Remove result and original script function + state.Pop(2) // Clean up - // Extract response data directly from the response object we passed - return s.buildResponse(response, result), nil + var modifiedResponse map[string]any + var scriptResult any + + if arr, ok := result.([]any); ok && len(arr) >= 2 { + scriptResult = arr[0] + if resp, ok := arr[1].(map[string]any); ok { + modifiedResponse = resp + } + } + + if modifiedResponse == nil { + scriptResult = result + modifiedResponse = response + } + + return s.buildResponse(modifiedResponse, scriptResult), nil } // buildResponse converts the Lua response object to a Go Response @@ -118,10 +121,6 @@ func (s *Sandbox) buildResponse(luaResp map[string]any, body any) *Response { resp.Headers[k] = str } } - } else if headers, ok := luaResp["headers"].(map[string]string); ok { - for k, v := range headers { - resp.Headers[k] = v - } } // Extract cookies @@ -161,18 +160,14 @@ func (s *Sandbox) buildResponse(luaResp map[string]any, body any) *Response { } } - // Extract metadata + // Extract metadata - simplified if metadata, ok := luaResp["metadata"].(map[string]any); ok { - for k, v := range metadata { - resp.Metadata[k] = v - } + maps.Copy(resp.Metadata, metadata) } - // Extract session data + // Extract session data - simplified if session, ok := luaResp["session"].(map[string]any); ok { - for k, v := range session { - resp.SessionData[k] = v - } + maps.Copy(resp.SessionData, session) } return resp @@ -180,7 +175,33 @@ func (s *Sandbox) buildResponse(luaResp map[string]any, body any) *Response { // registerCoreFunctions registers all built-in functions in the Lua state func (s *Sandbox) registerCoreFunctions(state *luajit.State) error { - // Register your native functions here - // This stays the same as your current implementation + if err := lualibs.RegisterCryptoFunctions(state); err != nil { + return err + } + + if err := lualibs.RegisterEnvFunctions(state); err != nil { + return err + } + + if err := lualibs.RegisterFSFunctions(state); err != nil { + return err + } + + if err := lualibs.RegisterHttpFunctions(state); err != nil { + return err + } + + if err := lualibs.RegisterPasswordFunctions(state); err != nil { + return err + } + + if err := lualibs.RegisterSQLiteFunctions(state); err != nil { + return err + } + + if err := lualibs.RegisterUtilFunctions(state); err != nil { + return err + } + return nil }