469 lines
17 KiB
Lua
469 lines
17 KiB
Lua
|
--[[
|
||
|
Mineysocket
|
||
|
Copyright (C) 2019 Robert Lieback <robertlieback@zetabyte.de>
|
||
|
|
||
|
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 <http://www.gnu.org/licenses/>.
|
||
|
--]]
|
||
|
|
||
|
-- 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")
|