From fa1f4894af8976e11d6211871f1208c5d52c02ca Mon Sep 17 00:00:00 2001 From: unknown <24964441+wsor4035@users.noreply.github.com> Date: Fri, 18 Feb 2022 17:29:31 -0500 Subject: [PATCH] add lua sandbox, tabs, stuff --- computers/api.lua | 133 +++++++++++++++++++++++++++++ computers/commands/exit/conf.json | 7 ++ computers/commands/exit/init.lua | 7 ++ computers/commands/expr/conf.json | 7 ++ computers/commands/expr/init.lua | 14 +++ computers/commands/nano/conf.json | 7 ++ computers/commands/nano/init.lua | 113 ++++++++++++++++++++++++ computers/commands/touch/conf.json | 4 +- computers/commands/touch/init.lua | 11 ++- computers/formspec.lua | 34 ++++++-- computers/gui.lua | 132 +++++++++++++++++++++++----- computers/init.lua | 3 +- 12 files changed, 436 insertions(+), 36 deletions(-) create mode 100644 computers/api.lua create mode 100644 computers/commands/exit/conf.json create mode 100644 computers/commands/exit/init.lua create mode 100644 computers/commands/expr/conf.json create mode 100644 computers/commands/expr/init.lua create mode 100644 computers/commands/nano/conf.json create mode 100644 computers/commands/nano/init.lua diff --git a/computers/api.lua b/computers/api.lua new file mode 100644 index 0000000..8364b92 --- /dev/null +++ b/computers/api.lua @@ -0,0 +1,133 @@ +--[[ + computers.sandbox code is from https://github.com/kikito/lua-sandbox + License MIT 2013 Enrique GarcĂ­a Cota + additionally modified by wsor to work with lua 5.1/luajit + modifications include: + * additional protections + * tweaking of structure + * modifications to the default env +]] +computers.sandbox = {} +computers.api = {} + +--default sandbox enviroment used if none is provided +computers.sandbox.default_env = {} + +([[ + _VERSION assert error ipairs next pairs + select tonumber tostring type unpack + + math.abs math.acos math.asin math.atan math.atan2 math.ceil + math.cos math.cosh math.deg math.exp math.fmod math.floor + math.frexp math.huge math.ldexp math.log math.log10 math.max + math.min math.modf math.pi math.pow math.rad math.random + math.sin math.sinh math.sqrt math.tan math.tanh + + os.clock os.difftime os.time + + string.byte string.char string.format + string.len string.lower string.reverse + string.sub string.upper + + table.insert table.maxn table.remove table.sort table.concat +]]):gsub('%S+', function(id) + local module, method = id:match('([^%.]+)%.([^%.]+)') + if module then + computers.sandbox.default_env[module] = computers.sandbox.default_env[module] or {} + computers.sandbox.default_env[module][method] = _G[module][method] + else + computers.sandbox.default_env[id] = _G[id] + end +end) + +--takes a code string, returns a sandboxed function to exicute +function computers.sandbox.loadstring(code, options) + local defaults = { + env = table.copy(computers.sandbox.default_env), + quota = 5000 + } + + options = options or {} + for k, v in pairs(defaults) do + if not options[k] then options[k] = v end + end + + assert(type(code) == "string", "[computers.sandbox]: passed is not a string") + + local env = options.env + env._G = env + + if code:byte(1) == 27 then + minetest.log("warning", "[computers.sandbox]: attempted bytecode execution termminated") + return nil, "bytecode disallowed" + end + + local f, err = loadstring(code) + if not f then return nil, err end + setfenv(f, env) + + if jit then jit.off(f, true) end + + local function timeout() + debug.sethook() + error("quota exceeded: " .. tostring(options.quota)) + end + + return function(...) + debug.sethook(timeout, "", options.quota) + local string_metatable = getmetatable("") + assert(string_metatable.__index == string, "[computers.sandbox]: error with the string metatable") + string_metatable.__index = env.string + + local status, ret = pcall(f, ...) + + debug.sethook() + string_metatable.__index = string + + if not status then error(ret) end + return ret + end + +end + +--supports string input or table env input with a default env of the base +function computers.sandbox.merge_env(nenv, denv) + local base_env = table.copy(denv or computers.sandbox.default_env) + + if type(nenv) == "table" then + for k, v in pairs(nenv) do + base_env[k] = base_env[k] or {} + for key, value in pairs(v) do + base_env[k][key] = value + end + end + elseif type(nenv) == "string" then + nenv:gsub('%S+', function(id) + local module, method = id:match('([^%.]+)%.([^%.]+)') + if module then + base_env[module] = base_env[module] or {} + base_env[module][method] = _G[module][method] + else + base_env[id] = _G[id] + end + end) + end + + return base_env +end + +function computers.api.get_dir_keyed_list(path, is_dir) + local files = minetest.get_dir_list(path, is_dir) + local keyed = {} + for _, file in pairs(files) do + keyed[file] = true + end + + return keyed +end + +function computers.api.chat_send_player(player, msg) + local name = player + if type(name) == "userdata" then name = player:get_player_name() end + minetest.chat_send_player(name, msg) +end \ No newline at end of file diff --git a/computers/commands/exit/conf.json b/computers/commands/exit/conf.json new file mode 100644 index 0000000..46e72c7 --- /dev/null +++ b/computers/commands/exit/conf.json @@ -0,0 +1,7 @@ +{ + "name": "exit", + "license": "MIT", + "version": 0.1, + "engine": 0.42, + "help": "exits the computer" +} \ No newline at end of file diff --git a/computers/commands/exit/init.lua b/computers/commands/exit/init.lua new file mode 100644 index 0000000..10d984b --- /dev/null +++ b/computers/commands/exit/init.lua @@ -0,0 +1,7 @@ +function exit(pos, input, data) + minetest.close_formspec(data.player:get_player_name(), "") + + return "you shouldnt see this" +end + +return exit \ No newline at end of file diff --git a/computers/commands/expr/conf.json b/computers/commands/expr/conf.json new file mode 100644 index 0000000..9390e68 --- /dev/null +++ b/computers/commands/expr/conf.json @@ -0,0 +1,7 @@ +{ + "name": "expr", + "license": "MIT", + "version": 0.1, + "engine": 0.42, + "help": "exicutes mathmatical expresions" +} \ No newline at end of file diff --git a/computers/commands/expr/init.lua b/computers/commands/expr/init.lua new file mode 100644 index 0000000..fd6b1c6 --- /dev/null +++ b/computers/commands/expr/init.lua @@ -0,0 +1,14 @@ +function expr(pos, input, data) + local output + local func = computers.sandbox.loadstring("return " .. input, {env={}}) + + if func then output = func() end + + if type(output) ~= "number" or output == nil then + return "Error: invalid or missing input" + end + + return tostring(output) +end + +return expr \ No newline at end of file diff --git a/computers/commands/nano/conf.json b/computers/commands/nano/conf.json new file mode 100644 index 0000000..284dd93 --- /dev/null +++ b/computers/commands/nano/conf.json @@ -0,0 +1,7 @@ +{ + "name": "nano", + "license": "MIT", + "version": 0.1, + "engine": 0.42, + "help": "allows editing of files" +} \ No newline at end of file diff --git a/computers/commands/nano/init.lua b/computers/commands/nano/init.lua new file mode 100644 index 0000000..a5b3e80 --- /dev/null +++ b/computers/commands/nano/init.lua @@ -0,0 +1,113 @@ +function nano(pos, input, data) + local path = computers.devicepath .. "/" .. minetest.hash_node_position(pos) .. data.element.pwd + local files = computers.api.get_dir_keyed_list(path, false) + + if not input then return "no file designated" end + if not files[input:split(" ")[1]] then return "file " .. input:split(" ")[1] .. " not found" end + if computers.formspec.get_index_by_name( + computers.formspec.registered_kast[data.player:get_player_name()], + "nano_btn" + ) then return "nano is already open, please close it" end + + local f = io.open(path .. "/" .. input:split(" ")[1]) + if not f then return "error reading file" end + local file = f:read("*all") + f:close() + + --clean up if there exists a legacy nano_ctn + local nctn = computers.formspec.get_index_by_name( + computers.formspec.registered_kast[data.player:get_player_name()], + "nano_ctn" + ) + if nctn then + local fs = computers.formspec.registered_kast[data.player:get_player_name()] + table.remove(fs, nctn) + computers.formspec.registered_kast[data.player:get_player_name()] = fs + minetest.chat_send_all(dump(fs)) + end + + local form = computers.gui.add_tab(data.player, "Nano", { + type = "container", + name = "nano_ctn", + state = 1, + x = 0, + y = 1, + { + type = "background", + x = 0, + y = 0, + w = 10, + h = 11, + texture_name = "[combine:16x16^[noalpha" + }, + { + type = "textarea", + x = 0, + y = 0, + w = 10, + h = 10, + name = "nano_editor", + --read_only = 1, + --label = "test", + default = file, + close_on_enter = false, + --this breaks the scrollbar + --[[ props = { + border = false, + } ]] + }, + { + type = "box", + x = 0, + y = 10, + w = 10, + h = 1, + color = "#ffffff" + }, + { + type = "field", + x = 0, + y = 10, + w = 10, + h = 1, + name = "nano_commands", + close_on_enter = false, + pwd = "", + props = { + border = false, + }, + on_event = function(form, player, element, value, fields) + if value == "save" and fields.nano_editor then + --minetest.chat_send_all(fields.nano_editor) + if #fields.nano_editor < 12000 then --12000 is luacheck line length of 120 * 100 lines + minetest.safe_file_write(path .. "/" .. input:split(" ")[1], fields.nano_editor) + else + computers.api.chat_send_player(player, minetest.colorize("red", "[Nano]: file is to long")) + end + elseif value == "exit" then + local btn = computers.formspec.get_index_by_name(form, "nano_btn") + local ctn = computers.formspec.get_index_by_name(form, "nano_ctn") + form[ctn].state = 1 + table.remove(form, btn) + --table.remove(form, ctn) cant actually remove it from within itself, therefore we hide it + local tindex = 0 + for index, tab in pairs(form.tabs) do + if tab == "nano" then tindex = index end + end + table.remove(form.tabs, tindex) + tindex = computers.formspec.get_index_by_name(form, "terminal_ctn") + + form[tindex].state = 0 + else + computers.api.chat_send_player(player, minetest.colorize("red", "[Nano]: invalid command")) + end + + return form + end, + }, + }) + + return form +end + +return nano \ No newline at end of file diff --git a/computers/commands/touch/conf.json b/computers/commands/touch/conf.json index 0d69882..dd2ebe1 100644 --- a/computers/commands/touch/conf.json +++ b/computers/commands/touch/conf.json @@ -1,7 +1,7 @@ { "name": "touch", "license": "MIT", - "version": 0.2, - "engine": 0.4, + "version": 0.3, + "engine": 0.42, "help": "creates a file in the current path" } \ No newline at end of file diff --git a/computers/commands/touch/init.lua b/computers/commands/touch/init.lua index 0e28c43..982eaeb 100644 --- a/computers/commands/touch/init.lua +++ b/computers/commands/touch/init.lua @@ -3,11 +3,14 @@ function touch(pos, input, data) --make dir to be safe minetest.mkdir(path) + if #minetest.get_dir_list(path, false) > 10 + and not minetest.check_player_privs(data.player, "computers_filesystem") then + return "ERROR: you have reached your max file limit" + end + if input and input ~= "" and not input:find("/") then - for _, item in pairs(minetest.get_dir_list(path, nil)) do - if item == input then - return "ERROR: trying to create already existing file/folder" - end + if computers.api.get_dir_keyed_list(path, nil)[input] then + return "ERROR: trying to create already existing file/folder" end minetest.safe_file_write(path .. "/" .. input, "") return "file " .. input .. " created" diff --git a/computers/formspec.lua b/computers/formspec.lua index f05e4a8..741b6a7 100644 --- a/computers/formspec.lua +++ b/computers/formspec.lua @@ -1,5 +1,5 @@ -local registered_astk = {} computers.formspec = {} +computers.formspec.registered_kast = {} computers.formspec.get_element_by_name = formspec_ast.get_element_by_name @@ -83,7 +83,7 @@ function computers.formspec.show_formspec(player, formname, fs) playername = player:get_player_name() end if type(fs) == "table" then - registered_astk[playername] = table.copy(fs) + computers.formspec.registered_kast[playername] = table.copy(fs) formspec = formspec_ast.unparse(computers.formspec.convert_to_ast(fs)) end @@ -94,16 +94,38 @@ minetest.register_on_player_receive_fields(function(player, formname, fields) if formname ~= "computers:gui" then return end local pname = player:get_player_name() - if fields.quit then registered_astk[pname] = nil return end + if fields.quit then computers.formspec.registered_kast[pname] = nil return end + + --[[ + trying to figure out what a player actually did can be a mess + input can validly be nil for a text field sometimes, othertimes it can come in first + when a user has actual selected a button. buttons only come in when selected, + so we override whatever the first key is if there a button since we are sure it was pressed + since we cant get types from fields, we rely on the button being named name_btn with _btn being a suffix + ]] local keys = {} - for key, val in pairs(fields) do table.insert(keys, key) end + local btn_override + for key, val in pairs(fields) do + table.insert(keys, key) + local split = key:split("_") + if #split >= 2 and split[2] == "btn" then btn_override = key end + end - local element = computers.formspec.get_element_by_name(registered_astk[pname], keys[1]) + local element = computers.formspec.get_element_by_name( + computers.formspec.registered_kast[pname], + btn_override or keys[1] + ) if element and element.on_event then --on_event(form, player, element) - local form = element.on_event(registered_astk[pname], player, element, fields[keys[1]]) + local form = element.on_event( + computers.formspec.registered_kast[pname], + player, + element, + fields[btn_override or keys[1]], + fields + ) if form then computers.formspec.show_formspec(player, formname, form) end end diff --git a/computers/gui.lua b/computers/gui.lua index df7bd72..ad5cf3a 100644 --- a/computers/gui.lua +++ b/computers/gui.lua @@ -1,29 +1,31 @@ ---local usage +computers.gui = {} + local futil = computers.formspec -function computers.load_gui(pos, node, clicker) - --minetest.chat_send_all("test") - local function select_btn(form, btn) - --to hardcoded - for _, obtn in pairs({"terminal", "browser"}) do - local cindex = futil.get_index_by_name(form, obtn .. "_btn") - form[cindex].props.bgimg = "kuto_button.png^[combine:16x16^[noalpha^[colorize:#ffffff70" - form[cindex].props.bgimg_hovered = "kuto_button.png^[combine:16x16^[noalpha^[colorize:#ffffff90" +local function select_btn(form, btn) + --to hardcoded + for _, obtn in pairs(form.tabs) do + local cindex = futil.get_index_by_name(form, obtn .. "_btn") + form[cindex].props.bgimg = "kuto_button.png^[combine:16x16^[noalpha^[colorize:#ffffff70" + form[cindex].props.bgimg_hovered = "kuto_button.png^[combine:16x16^[noalpha^[colorize:#ffffff90" - cindex = futil.get_index_by_name(form, obtn .. "_ctn") - form[cindex].state = 1 - end - - local cindex = futil.get_index_by_name(form, btn .. "_btn") - form[cindex].props.bgimg = "kuto_button.png^[combine:16x16^[noalpha^[colorize:#ffffff20" - form[cindex].props.bgimg_hovered = "kuto_button.png^[combine:16x16^[noalpha^[colorize:#ffffff40" - - local aindex = futil.get_index_by_name(form, btn .. "_ctn") - form[aindex].state = 0 + cindex = futil.get_index_by_name(form, obtn .. "_ctn") + form[cindex].state = 1 end + local cindex = futil.get_index_by_name(form, btn .. "_btn") + form[cindex].props.bgimg = "kuto_button.png^[combine:16x16^[noalpha^[colorize:#ffffff20" + form[cindex].props.bgimg_hovered = "kuto_button.png^[combine:16x16^[noalpha^[colorize:#ffffff40" + + local aindex = futil.get_index_by_name(form, btn .. "_ctn") + form[aindex].state = 0 +end + +function computers.gui.load(pos, node, clicker) + local formspec = { formspec_version = 4, + tabs = {"terminal", "browser"}, { type = "size", w = 10, @@ -150,6 +152,8 @@ function computers.load_gui(pos, node, clicker) local output = computers.registered_commands[cdata[1]](pos, cdata[2], pass_table) if output and type(output) == "string" then form[cindex][eindex].default = text .. output .. "\n" .. "user:~" .. element.pwd .."$" .." " + elseif output and type(output) == "table" then + form = output end else form[cindex][eindex].default = text .. value .. @@ -188,10 +192,31 @@ function computers.load_gui(pos, node, clicker) h = 2, name = "test_btn", label = "test btn", - on_event = function(form, player, element) - local cindex = futil.get_index_by_name(form, "browser_ctn") - local eindex = futil.get_index_by_name(form[cindex], "test_btn") - form[cindex][eindex] = {type = "label", x=1, y=3, label = "test button label"} + on_event = function(_, player, element) + --local cindex = futil.get_index_by_name(form, "browser_ctn") + --local eindex = futil.get_index_by_name(form[cindex], "test_btn") + --form[cindex][eindex] = {type = "label", x=1, y=3, label = "test button label"} + local form = computers.gui.add_tab(player, "Tname", { + type = "container", + name = "tname_ctn", + state = 1, + x = 0, + y = 1, + { + type = "background", + x = 0, + y = 0, + w = 10, + h = 11, + texture_name = "kuto_button.png^[combine:16x16^[noalpha^[colorize:#ffffff70" + }, + { + type = "label", + x = 1, + y = 1.5, + label = "Tname pane", + }, + }) return form end, @@ -206,4 +231,65 @@ function computers.load_gui(pos, node, clicker) } futil.show_formspec(clicker, "computers:gui", formspec) +end + +--legacy compat +computers.load_gui = computers.gui.load + +--note you can create to many pages thuse overflowing the formspec, need to be fixed +function computers.gui.add_tab(player, tname, tab) + local name = player + if type(player) == "userdata" then name = player:get_player_name() end + + assert(tab, "[computers.sandbox]: new tab for " .. name .. " not found") + assert(tab.type == "container", "[computers.sandbox]: invalid new tab format for " .. name) + assert(tab.name:split("_")[2] == "ctn", "[computers.sandbox]: invalid tab name for " .. name) + assert(tab.x == 0 and tab.y == 1, "[computers.sandbox]: invalid tab name for " .. name) + + + local formspec = table.copy(computers.formspec.registered_kast[name]) + local fs = {} + local btn + + for key, val in pairs(formspec) do + if type(key) == "string" then + fs[key] = val + elseif type(val) == "table" then + if val.name and #val.name:split("_") >= 2 and val.name:split("_")[2] == "btn" then + btn = 1 + elseif btn and btn == 1 then + table.insert(fs, { + type = "button", + x = #formspec.tabs*2, + y = 0, + w = 2, + h = 1, + name = tname:lower() .. "_btn", + label = tname, + on_event = function(form, _, element) + select_btn(form, tname:lower()) + + return form + end, + props = { + border = false, + bgimg = "kuto_button.png^[combine:16x16^[noalpha^[colorize:#ffffff70", + bgimg_hovered = "kuto_button.png^[combine:16x16^[noalpha^[colorize:#ffffff90", + bgimg_middle = "4,4", + } + }) + + btn = nil + end + + table.insert(fs, val) + + end + + end + + table.insert(fs, tab) + table.insert(fs.tabs, tname:lower()) + + return fs end \ No newline at end of file diff --git a/computers/init.lua b/computers/init.lua index 7e3f3a3..4bbbb66 100644 --- a/computers/init.lua +++ b/computers/init.lua @@ -8,7 +8,7 @@ minetest.mkdir(computers.devicepath) --make sure it exists minetest.mkdir(computers.networkpath) --make sure it exists computers.os = { - version = 0.41, + version = 0.42, name = "kuto", authors = {"wsor", "luk3yx"}, license = "MIT", @@ -16,6 +16,7 @@ computers.os = { dofile(computers.modpath .. "/old_stuff/init.lua") +dofile(computers.modpath .. "/api.lua") dofile(computers.modpath .. "/formspec.lua") dofile(computers.modpath .. "/commands.lua") dofile(computers.modpath .. "/gui.lua")