564 lines
14 KiB
Lua
Raw Normal View History

2021-03-27 07:16:30 -04:00
--[[
ITB (insidethebox) minetest game - Copyright (C) 2017-2018 sofar & nore
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public License
as published by the Free Software Foundation; either version 2.1
of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free
Software Foundation, Inc., 59 Temple Place, Suite 330, Boston,
MA 02111-1307 USA
]]--
--[[
terminal - an interactive terminal
]]--
fsc = {}
local _data = {}
local SRNG = SecureRandom()
assert(SRNG)
local function make_new_random_id()
local s = SRNG:next_bytes(16)
return s:gsub(".", function(c) return string.format("%02x", string.byte(c)) end)
end
function fsc.show(name, formspec, context, callback)
assert(name)
assert(formspec)
assert(callback)
if not context then
context = {}
end
-- erase old context!
local id = "fsc:" .. make_new_random_id()
_data[name] = {
id = id,
name = name,
context = context,
callback = callback,
}
minetest.show_formspec(name, id, formspec)
end
minetest.register_on_player_receive_fields(function(player, formname, fields)
if not formname:match("fsc:") then
return false
end
local name = player:get_player_name()
local data = _data[name]
if not data then
minetest.log("warning", "fsc: no data for formspec sent by " .. name)
minetest.close_formspec(name, formname)
return
end
if data.id ~= formname then
minetest.log("warning", "fsc: invalid id for formspec sent by " .. name)
minetest.close_formspec(name, formname)
_data[name] = nil
return
end
if data.name ~= name then
minetest.log("error", "fsc: possible hash collision or exploit (name mismatch)")
minetest.close_formspec(name, formname)
_data[name] = nil
return
end
if data then
if data.callback(player, fields, data.context) then
minetest.close_formspec(name, formname)
_data[name] = nil
elseif fields.quit then
_data[name] = nil
end
end
end)
minetest.register_on_leaveplayer(function(player)
_data[player:get_player_name()] = nil
end)
log = {}
function log.fs_data(player, name, formname, fields)
assert(player or name)
local pos = "(unknown pos)"
if name and not player then
player = minetest.get_player_by_name(name)
end
if player then
pos = minetest.pos_to_string(vector.floor(player:get_pos()))
end
if not name and player then
name = player:get_player_name()
end
local f = ""
for k, _ in pairs(fields) do
if f ~= "" then
f = f .. ", "
end
f = f .. k
end
minetest.log("error", "invalid formspec data: player " .. name .. " at " ..
pos .. " sends " .. formname .. ": [" .. f .. "]")
end
local term = {}
local function get_cmd_params(line)
local cmd = ""
local params = ""
for w in line:gmatch("%w+") do
if cmd == "" then
cmd = w
elseif params == "" then
params = w
else
params = params .. " " .. w
end
end
return cmd, params
end
term.help = {
append = "append text to a file",
clear = "clear the output",
echo = "echoes the input back to you",
help = "display help information for commands",
list = "list available files",
lock = "lock the terminal",
read = "read the content of a file",
remove = "removes a file",
unlock = "unlocks the terminal",
write = "write text to a file",
edit = "edits a file in an editor",
}
local function make_formspec(output, prompt)
local f =
"size[12,8]" ..
"field_close_on_enter[input;false]" ..
"textlist[0.4,0.5;11,6;output;"
local c = 1
if output then
for part in output:gmatch("([^\r\n]*)\n?") do
f = f .. minetest.formspec_escape(part) .. ","
c = c + 1
end
end
f = f .. minetest.formspec_escape(prompt) .. ";" .. c .. ";false]"
f = f .. "field[0.7,7;11.2,1;input;;]"
return f
end
term.commands = {
clear = function(output, params, c)
return ""
end,
append = function(output, params, c)
if not c.rw then
return output .. "\nError: No write access"
end
local what, _ = get_cmd_params(params)
if what == "" then
return output .. "\nError: Missing file name"
end
c.writing = what
return output .. "\nWriting \"" .. what .. "\". Enter STOP on a line by itself to finish"
end,
write = function(output, params, c)
return output .. "\nError: No change access"
--[[if not c.rw then
return output .. "\nError: No write access"
end
local what, _ = get_cmd_params(params)
if what == "" then
return output .. "\nError: Missing file name"
end
c.writing = what
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
if meta_files and meta_files ~= "" then
local files = minetest.parse_json(meta_files) or {}
if files and files[what] then
files[what] = ""
meta:set_string("files", minetest.write_json(files))
meta:mark_as_private("files")
end
end
return output .. "\nWriting \"" .. what .. "\". Enter STOP on a line by itself to finish" --]]
end,
edit = function(output, params, c)
return output .. "\nError: No change access"
--[[if not c.rw then
return output .. "\nError: No write access"
end
local what, _ = get_cmd_params(params)
if what == "" then
return output .. "\nError: Missing file name"
end
c.what = what
c.output = output
local text = ""
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
if meta_files and meta_files ~= "" then
local files = minetest.parse_json(meta_files) or {}
if files and files[what] then
text = files[what]
end
end
fsc.show(c.name, "size[12,8]" ..
"textarea[0.5,0.5;11.5,7.0;text;text;" ..
minetest.formspec_escape(text) .. "]" ..
"button[5.2,7.2;1.6,0.5;exit;Save]",
c,
term.edit)
return false --]]
end,
remove = function(output, params, c)
return output .. "\nError: No change access"
--[[if not c.rw then
return output .. "\nError: No write access"
end
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
if not meta_files or meta_files == "" then
return output .. "\nError: No such file"
end
local files = minetest.parse_json(meta_files) or {}
local first, _ = get_cmd_params(params)
if files[first] then
files[first] = nil
else
return output .. "\nError: No such file"
end
meta:set_string("files", minetest.write_json(files))
meta:mark_as_private("files")
return output .. "\nRemoved \"" .. first .. "\"" --]]
end,
list = function(output, params, c)
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
local files
if not meta_files or meta_files == "" then
return output .. "\nError: No files found"
end
files = minetest.parse_json(meta_files) or {}
if not files then
return output .. "\nError: No files found"
end
for k, _ in pairs(files) do
output = output .. "\n" .. k
end
return output
end,
echo = function(output, params, c)
return output .. "\n" .. params
end,
read = function(output, params, c)
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
if not meta_files or meta_files == "" then
return output .. "\nError: No such file"
end
local files = minetest.parse_json(meta_files) or {}
local first, _ = get_cmd_params(params)
if files[first] then
if first == "rules" then
rules.show(c.name, "player")
return false
end
return output .. "\n" .. files[first]
else
return output .. "\nError: No such file"
end
end,
lock = function(output, params, c)
if not c.rw then
return output .. "\nError: no write access"
end
local meta = minetest.get_meta(c.pos)
meta:set_int("locked", 1)
--meta:mark_as_private("locked")
return output .. "\n" .. "Terminal locked"
end,
unlock = function(output, params, c)
--return output .. "\n" .. "Error: unable to connect to authentication service"
if not c.rw then
return output .. "\n" .. "Error: invalid credentials"
end
local meta = minetest.get_meta(c.pos)
meta:set_int("locked", 0)
return output .. "\n" .. "Terminal unlocked"
end,
help = function(output, params, c)
if params ~= "" then
local h, _ = get_cmd_params(params)
if term.help[h] then
return output .. "\n" .. term.help[h]
else
return output .. "\nError: No help for \"" .. h .. "\""
end
end
local o = ""
local ot = {}
for k, _ in pairs(term.help) do
ot[#ot + 1] = k
end
table.sort(ot)
for _, v in ipairs(ot) do
o = o .. " " .. v .. "\n"
end
return output .. "\n" ..
"Available commands:\n" ..
o ..
"Type `help <command>` for more help about that command"
end,
}
function term.recv(player, fields, context)
-- input validation
local name = player:get_player_name()
local c = context
if not c or not c.pos then
log.fs_data(player, name, "term.recv/context", fields)
return true
end
local line = fields.input
if line and line ~= "" then
local output = c.output or ""
minetest.sound_play("terminal_keyboard_clicks", {pos = c.pos})
if c.writing then
-- this shouldn't get reached, but just to be safe, check ro
if not c.rw then
c.writing = nil
output = output .. "\n" .. line
output = output .. "\nError: no write access"
c.output = output
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
-- are we writing a file?
if line == "STOP" then
-- done writing a file
c.writing = nil
output = output .. "\n" .. line
c.output = output
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
local files = {}
if not meta_files or meta_files == "" then
files[c.writing] = line
else
files = minetest.parse_json(meta_files) or {}
if not files[c.writing] then
files[c.writing] = line
else
files[c.writing] = files[c.writing] .. "\n" .. line
end
end
if string.len(files[c.writing]) < 16384 then
local json = minetest.write_json(files)
if string.len(json) < 49152 then
meta:set_string("files", json)
meta:mark_as_private("files")
output = output .. "\n" .. line
else
output = output .. "\n" .. "Error: no space left on device"
end
else
output = output .. "\n" .. "Error: maximum file length exceeded"
end
c.output = output
fsc.show(name,
make_formspec(output, ""),
c,
term.recv)
return
else
-- else parse cmd
output = output .. "\n> " .. line
local meta = minetest.get_meta(c.pos)
local cmd, params = get_cmd_params(line)
if meta:get_int("locked") == 1 and cmd ~= "unlock" then
output = output .. "\nError: Terminal locked, type \"unlock\" to unlock it"
c.output = output
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local fn = term.commands[cmd]
if fn then
output = fn(output, params, c)
else
output = output .. "\n" .. "Error: Syntax Error. Try \"help\""
end
if output ~= false then
c.output = output
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
end
return
end
elseif fields.quit then
minetest.sound_play("terminal_power_off", {pos = c.pos})
return true
elseif fields.output then
-- CHG events - do not return true
return
elseif fields.input then
-- KEYBOARD events - do not return true
return
end
log.fs_data(player, name, "term.recv/default", fields)
return true
end
function term.edit(player, fields, context)
if not fields.text then
return true
end
local name = player:get_player_name()
local c = context
if not c or not c.pos or not c.output then
log.fs_data(player, name, "term.edit/terminal", fields)
return true
end
local output = c.output
if not c.what then
output = output .. "\n" .. "Error: no such file\n"
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local meta = minetest.get_meta(c.pos)
local meta_files = meta:get_string("files")
local files
files = minetest.parse_json(meta_files) or {}
files[c.what] = fields.text
-- validate it fits
local json = minetest.write_json(files)
if string.len(json) < 49152 then
meta:set_string("files", json)
meta:mark_as_private("files")
output = output .. "\n" .. "Wrote: " .. c.what .. "\n"
else
output = output .. "\n" .. "Error: no space left on device\n"
end
c.output = output
fsc.show(name,
make_formspec(output, "> "),
c,
term.recv)
return
end
local terminal_use = function(pos, node, clicker, itemstack, pointed_thing)
if not clicker then
return
end
local name = clicker:get_player_name()
local context = {
pos = pos,
rw = false,
output = "",
name = name,
}
context.rw = true
-- send formspec to player
fsc.show(name,
make_formspec(nil, "> "),
context,
term.recv)
minetest.sound_play("terminal_power_on", {pos = pos})
-- trigger on first use
local meta = minetest.get_meta(pos)
if meta:get_int("locked") ~= 1 then
--mech.trigger(pos)
--minetest.after(1.0, mech.untrigger, pos)
end
end
minetest.register_node("terminal:terminal", {
description = "Interactive terminal console emulator access interface unit controller",
--drawtype = "mesh",
mesh = "terminal.obj",
groups = {mech = 1, trigger = 1},
tiles = {
--{name = "terminal_base.png"},
{name = "terminal_idle.png", animation = {type = "vertical_frames", aspect_w = 14, aspect_h = 13, length = 4.0}},
},
paramtype = "light",
paramtype2 = "facedir",
on_trigger = function(pos)
local meta = minetest.get_meta(pos)
minetest.sound_play("terminal_power_on", {pos = pos})
meta:set_int("locked", 0)
meta:mark_as_private("locked")
end,
on_untrigger = function(pos)
local meta = minetest.get_meta(pos)
minetest.sound_play("terminal_power_off", {pos = pos})
meta:set_int("locked", 1)
meta:mark_as_private("locked")
end,
on_rightclick = terminal_use,
--sounds = default.node_sound_metal_defaults(),
sunlight_propagates = true,
is_ground_content = false,
light_source = 0,--default.LIGHT_MAX,
})