--[[ Mineysocket Copyright (C) 2019 Robert Lieback This program 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 3 of the License, or (at your option) any later version. This program 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 General Public License along with this program. If not, see . --]] -- Load external libs local ie if minetest.request_insecure_environment then ie = minetest.request_insecure_environment() end if not ie then error("mineysocket has to be added to the secure.trusted_mods in minetest.conf") end mineysocket = {} -- global namespace -- just a logging function mineysocket.log = function(level, text, ip, port) -- if mineysocket.debug or level ~= "action" then if text then if ip and port then minetest.log(level, "mineysocket: " .. text .. " from " .. ip .. ":" .. port) else minetest.log(level, "mineysocket: " .. ": " .. text) end end -- end end mineysocket.log("action", "Starting to load mod") --[[ mineysocket.capture = function(cmd, raw) local f = assert(io.popen(cmd, 'r')) local s = assert(f:read('*a')) f:close() if raw then return s end s = string.gsub(s, '^%s+', '') s = string.gsub(s, '%s+$', '') s = string.gsub(s, '[\n\r]+', ' ') return s end local ttt = mineysocket.capture("ifconfig", false) mineysocket.log("action", "TEST: " .. ttt) ]]-- -- local sh = require('sh') mineysocket.log("action", os.getenv("HOSTNAME")) mineysocket.log("action", os.getenv("IPV4")) -- configuration mineysocket.host_ip = minetest.settings:get("mineysocket.host_ip") mineysocket.host_port = minetest.settings:get("mineysocket.host_port") -- Workaround for bug, where default values return only nil if not mineysocket.host_ip then mineysocket.host_ip = os.getenv("IPV4") end if not mineysocket.host_port then mineysocket.host_port = 29999 end mineysocket.debug = false -- set to true to show all log levels mineysocket.max_clients = 10 local luasocket = ie.require("socket.core") if not luasocket then error("luasocket is not installed or was not found...") end -- setup network server local server, err = luasocket.tcp() if not server then mineysocket.log("action", err) error("exit") end local bind, err = server:bind(mineysocket.host_ip, mineysocket.host_port) if not bind then error("mineysocket: " .. err) end local listen, err = server:listen(mineysocket.max_clients) if not listen then error("mineysocket: Socket listen error: " .. err) end mineysocket.log("action", "listening on " .. mineysocket.host_ip .. ":" .. tostring(mineysocket.host_port)) server:settimeout(0) mineysocket.host_ip, mineysocket.host_port = server:getsockname() if not mineysocket.host_ip or not mineysocket.host_port then error("mineysocket: Couldn't open secrver port!") end mineysocket["socket_clients"] = {} -- a table with all connected clients with there options -- receive network data and process them mineysocket.log("action", "Installing socket receiver...") minetest.register_globalstep(function(dtime) mineysocket.receive() end) mineysocket.log("action", "Installing socket receiver...DONE") -- Clean shutdown mineysocket.log("action", "Installing shutdown cleaning...") minetest.register_on_shutdown(function() mineysocket.log("action", "mineysocket: Closing port...") for clientid, client in pairs(mineysocket["socket_clients"]) do mineysocket["socket_clients"][clientid].socket:close() end server:close() end) mineysocket.log("action", "Installing shutdown cleaning...DONE") -- receive data from clients mineysocket.log("action", "Describing socket receiving function...") mineysocket.receive = function() local data, ip, port, clientid, client, err local result = false -- look for new client connections client, err = server:accept() if client then ip, port = client:getpeername() clientid = ip .. ":" .. port mineysocket.log("action", "New connection from " .. ip .. " " .. port) client:settimeout(0) -- register the new client if not mineysocket["socket_clients"][clientid] then mineysocket["socket_clients"][clientid] = {} mineysocket["socket_clients"][clientid].socket = client mineysocket["socket_clients"][clientid].last_message = minetest.get_server_uptime() mineysocket["socket_clients"][clientid].buffer = "" mineysocket["socket_clients"][clientid].eom = nil if ip == "127.0.0.1" then -- skip authentication for 127.0.0.1 mineysocket["socket_clients"][clientid].auth = true mineysocket["socket_clients"][clientid].playername = "localhost" mineysocket["socket_clients"][clientid].events = {} else mineysocket["socket_clients"][clientid].auth = false end end else if err ~= "timeout" then mineysocket.log("error", "Connection error \"" .. err .. "\"") client:close() end end -- receive data for clientid, client in pairs(mineysocket["socket_clients"]) do local complete_data, err, data = mineysocket["socket_clients"][clientid].socket:receive("*a") -- there are never complete_data, cause we don't receive lines -- Note: err is "timeout" every time when there are no client data, cause we set timeout to 0 and -- we don't want to wait and block lua/minetest for clients to send data if err ~= "timeout" then mineysocket["socket_clients"][clientid].socket:close() -- cleanup if err == "closed" then mineysocket["socket_clients"][clientid] = nil mineysocket.log("action", "Connection to ".. clientid .." was closed") return else mineysocket.log("action", err) end end if data and data ~= "" then -- store time of the last message for cleanup of old connection mineysocket["socket_clients"][clientid].last_message = minetest.get_server_uptime() if not string.find(data, "\n") then -- fill a buffer and wait for the linebreak if not mineysocket["socket_clients"][clientid].buffer then mineysocket["socket_clients"][clientid].buffer = data else mineysocket["socket_clients"][clientid].buffer = mineysocket["socket_clients"][clientid].buffer .. data end if mineysocket["socket_clients"][clientid].auth == false then -- limit buffer size for unauthenticated connections if mineysocket["socket_clients"][clientid].buffer and string.len(mineysocket["socket_clients"][clientid].buffer) + string.len(data) > 10 then mineysocket["socket_clients"][clientid].buffer = nil end end mineysocket.receive() return else -- get data from buffer and reset em if mineysocket["socket_clients"][clientid]["buffer"] then data = mineysocket["socket_clients"][clientid].buffer .. data mineysocket["socket_clients"][clientid].buffer = nil end mineysocket.log("action", "Received: \n" .. data) -- we try to find the eom message terminator for this session if mineysocket["socket_clients"][clientid].eom == nil then if string.sub(data, -2) == "\r\n" then mineysocket["socket_clients"][clientid].eom = "\r\n" else mineysocket["socket_clients"][clientid].eom = "\n" end end -- simple alive check if data == "ping" .. mineysocket["socket_clients"][clientid].eom then mineysocket["socket_clients"][clientid].socket:send("pong" .. mineysocket["socket_clients"][clientid].eom) return end -- parse data as json local status, input = pcall(minetest.parse_json, data) if not status then mineysocket.log("error", minetest.write_json({ error = input })) mineysocket.log("error", "JSON-Error: " .. input, ip, port) mineysocket.send(clientid, minetest.write_json({ error = "JSON decode error - " .. input })) return end -- is it a known client, or do we need authentication? if mineysocket["socket_clients"][clientid].auth == true then ---------------------------- -- commands: ---------------------------- -- we run lua code if input["lua"] then result = run_lua(input, clientid, ip, port) end -- append event to callback list if input["register_event"] then result = mineysocket.register_event(clientid, input["register_event"]) end -- append event to callback list if input["unregister_event"] then result = mineysocket.unregister_event(clientid, input["unregister_event"]) end -- handle reauthentication if input["playername"] and input["password"] then result = mineysocket.authenticate(input, clientid, ip, port, mineysocket["socket_clients"][clientid].socket) end -- reattach id if input["id"] and result ~= false then result["id"] = input["id"] end -- send result if result ~= false then mineysocket.send(clientid, minetest.write_json(result)) else mineysocket.send(clientid, minetest.write_json({ error = "Unknown command" })) end else -- we need authentication if input["playername"] and input["password"] then mineysocket.send(clientid, minetest.write_json(mineysocket.authenticate(input, clientid, ip, port, mineysocket["socket_clients"][clientid].socket))) else mineysocket.send(clientid, minetest.write_json({ error = "Unknown command" })) end end end end end end mineysocket.log("action", "Describing socket receiving function...DONE") -- run lua code send by the client mineysocket.log("action", "Function to run lua code...") function run_lua(input, clientid, ip, port) local start_time, err local output = {} start_time = minetest.get_server_uptime() -- log the (shortend) code if string.len(input["lua"]) > 120 then mineysocket.log("action", "execute: " .. string.sub(input["lua"], 0, 120) .. " ...", ip, port) else mineysocket.log("action", "execute: " .. input["lua"], ip, port) end -- run local f, syntaxError = loadstring(input["lua"]) -- todo: is there a way to get also warning like "Undeclared global variable ... accessed at ..."? if f then local status, result1, result2, result3, result4, result5 = pcall(f, clientid) -- Get the clientid with "...". Example: "mineysocket.send(..., output)" -- is there a more elegant way for unlimited results? if status then output["result"] = { result1, result2, result3, result4, result5 } if mineysocket.debug then local json_output = minetest.write_json(output) if string.len(json_output) > 120 then mineysocket.log("action", string.sub(json_output, 0, 120) .. " ..." .. " in " .. (minetest.get_server_uptime() - start_time) .. " seconds", ip, port) else mineysocket.log("action", json_output .. " in " .. (minetest.get_server_uptime() - start_time) .. " seconds", ip, port) end end return output else err = result1 end else err = syntaxError end -- send lua errors if err then output["error"] = err mineysocket.log("error", "Error " .. err .. " in command", ip, port) return output end end mineysocket.log("action", "Function to run lua code...DONE") -- authenticate clients mineysocket.log("action", "Function to authenticate...") mineysocket.authenticate = function(input, clientid, ip, port, socket) local player = minetest.get_auth_handler().get_auth(input["playername"]) -- we skip authentication for 127.0.0.1 and just accept everything if ip == "127.0.0.1" then mineysocket.log("action", "Player '" .. input["playername"] .. "' connected successful", ip, port) mineysocket["socket_clients"][clientid].playername = input["playername"] return { result = { "auth_ok", clientid }, id = "auth" } else -- others need a valid playername and password if player and minetest.check_password_entry(input["playername"], player['password'], input["password"]) and minetest.check_player_privs(input["playername"], { server = true }) then mineysocket.log("action", "Player '" .. input["playername"] .. "' authentication successful", ip, port) mineysocket["socket_clients"][clientid].auth = true mineysocket["socket_clients"][clientid].playername = input["playername"] mineysocket["socket_clients"][clientid].events = {} return { result = { "auth_ok", clientid }, id = "auth" } else mineysocket.log("error", "Wrong playername ('" .. input["playername"] .. "') or password", ip, port) mineysocket["socket_clients"][clientid].auth = false return { error = "authentication error" } end end end mineysocket.log("action", "Function to authenticate...DONE") -- send data to the client mineysocket.log("action", "Function to send data to client...") mineysocket.send = function(clientid, data) local data = data .. mineysocket["socket_clients"][clientid]["eom"] -- eom is the terminator local size = string.len(data) local chunk_size = 4096 if size < chunk_size then -- we send in one package mineysocket["socket_clients"][clientid].socket:send(data) else -- we split into multiple packages for i = 0, math.floor(size / chunk_size) do mineysocket["socket_clients"][clientid].socket:send( string.sub(data, i * chunk_size, chunk_size + (i * chunk_size) - 1) ) luasocket.sleep(0.001) -- Or buffer fills to fast -- todo: Protocol change, that every chunked message needs a response before sending the next end end end mineysocket.log("action", "Function to send data to client...") -- register for event mineysocket.log("action", "Function to register events...") mineysocket.register_event = function(clientid, eventname) mineysocket["socket_clients"][clientid].events[#mineysocket["socket_clients"][clientid].events+1] = eventname return { result = "ok" } end mineysocket.log("action", "Function to register events...DONE") -- unregister for event mineysocket.log("action", "Function to register unevents...") mineysocket.unregister_event = function(clientid, eventname) for index, value in pairs(mineysocket["socket_clients"][clientid].events) do if value == eventname then table.remove( mineysocket["socket_clients"][clientid].events, index ) break end end return { result = "ok" } end mineysocket.log("action", "Function to register unevents...DONE") -- send event data to clients, who are registered for this event mineysocket.log("action", "Function to send events...") mineysocket.send_event = function(data) for clientid, values in pairs(mineysocket["socket_clients"]) do local client_events = mineysocket["socket_clients"][clientid].events for _, event_data in ipairs(client_events) do local registered_event_name = event_data["event"] local received_event_name = data["event"][1] if registered_event_name == received_event_name then mineysocket.log("action", "Sending event: " .. received_event_name) mineysocket.send(clientid, minetest.write_json(data)) break end end end end mineysocket.log("action", "Function to send events...DONE") -- BEGIN global event registration mineysocket.log("action", "Registering global fucntions...") minetest.register_on_shutdown(function() mineysocket.send_event({ event = { "shutdown" } }) end) minetest.register_on_player_hpchange(function(player, hp_change, reason) mineysocket.send_event({ event = { "player_hpchanged", player:get_player_name(), hp_change, reason } }) end, false) minetest.register_on_dieplayer(function(player, reason) mineysocket.send_event({ event = { "player_died", player:get_player_name(), reason } }) end) minetest.register_on_respawnplayer(function(player) mineysocket.send_event({ event = { "player_respawned", player:get_player_name() } }) end) minetest.register_on_joinplayer(function(player) mineysocket.send_event({ event = { "player_joined", player:get_player_name() } }) end) minetest.register_on_leaveplayer(function(player, timed_out) mineysocket.send_event({ event = { "player_left", player:get_player_name(), timed_out } }) end) minetest.register_on_authplayer(function(name, ip) mineysocket.send_event({ event = { "auth_failed", name, ip } }) end) minetest.register_on_cheat(function(player, cheat) mineysocket.send_event({ event = { "player_cheated", player:get_player_name(), cheat } }) end) minetest.register_on_chat_message(function(name, message) mineysocket.send_event({ event = { "chat_message", name, message } }) end) mineysocket.log("action", "Registering global fucntions...DONE") -- END global event registration minetest.log("action", "Initialization - DONE")