diff --git a/raspberryjammod/init.lua b/raspberryjammod/init.lua index ae086a9..0163d92 100644 --- a/raspberryjammod/init.lua +++ b/raspberryjammod/init.lua @@ -8,7 +8,6 @@ local source = debug.getinfo(1).source:sub(2) -- Detect windows via backslashes in paths local mypath = minetest.get_modpath(minetest.get_current_modname()) local is_windows = (nil ~= string.find(package.path..package.cpath..source..mypath, "%\\%?")) -print(package.cpath) local path_separator if is_windows then path_separator = "\\" @@ -41,6 +40,7 @@ local settings = Settings(mypath .. path_separator .. "settings.conf") python_interpreter = settings:get("python") if not python_interpreter then python_interpreter = "python" end local local_only = settings:get_bool("local_only") +local ws = settings:get_bool("support_websockets") local server if local_only then @@ -48,36 +48,65 @@ if local_only then else server = socket.bind("*", 4711) end + server:setoption('tcp-nodelay',true) server:settimeout(0) +local ws_server = nil + +if ws then + tools = require("tools") + if local_only then + ws_server = socket.bind("127.0.0.1", 14711) + else + ws_server = socket.bind("*", 14711) + end + ws_server:setoption('tcp-nodelay',true) + ws_server:settimeout(0) +end + + + minetest.register_globalstep(function(dtime) - local newclient,err = server:accept() - if not err then - newclient:settimeout(0) - table.insert(socket_client_list, newclient) - minetest.log("action", "RJM socket client connected") + local newclient,err + + if server then + newclient,err = server:accept() + if not err then + newclient:settimeout(0) + table.insert(socket_client_list, + {client=newclient,handler=safe_handle_command,read_mode="*l"}) + minetest.log("action", "RJM socket client connected") + end + end + if ws_server then + newclient,err = ws_server:accept() + if not err then + newclient:settimeout(0) + table.insert(socket_client_list, + {client=newclient,handler=handle_websocket_header,ws={},read_mode="*l"}) + minetest.log("action", "RJM websocket client attempting handshake") + end end for i = 1, #socket_client_list do - local err = false + err = false local line local finished = false while not err do - line,err = socket_client_list[i]:receive() + local source = socket_client_list[i] + line,err = source.client:receive(source.read_mode) if err == "closed" then table.remove(socket_client_list,i) minetest.log("action", "RJM socket client disconnected") finished = true elseif not err then - local status - status, err = pcall(function() - local response = handle_command(line) - if response then socket_client_list[i]:send(response.."\n") end - end) - if not status then - socket_client_list[i]:close() - minetest.log("error", "Error "..err.." in command: RJM socket client disconnected") + err = source:handler(line) + if err then + source.client:close() + if err ~= "closed" then + minetest.log("error", "Error "..err.." in command: RJM socket client disconnected") + end finished = true err = "handling" end @@ -96,7 +125,7 @@ minetest.register_on_shutdown(function() end minetest.log("action", "RJM socket clients disconnected") for i = 1, #socket_client_list do - socket_client_list[i]:close() + socket_client_list[i].client:close() end socket_client_list = {} player_table = {} @@ -400,6 +429,14 @@ function kill(window_identifier) os.execute('taskkill /F /FI "WINDOWTITLE eq ' .. window_identifier .. '"') end +function safe_handle_command(source,line) + local status, err = pcall(function() + local response = handle_command(line) + if response then source.client:send(response.."\n") end + end) + return err +end + function handle_command(line) local cmd, argtext = line:match("^([^(]+)%((.*)%)") if not cmd then return end @@ -423,4 +460,170 @@ function handle_command(line) return nil end +function complete_data(source,data) + local needed = tonumber(source.read_mode) + local have = data:len() + + if source.ws.saved_data then + source.ws.saved_data = source.ws.saved_data .. data + else + source.ws.saved_data = data + end + + if have >= needed then + local out = source.ws.saved_data + source.ws.saved_data = nil + return out + else + source.read_mode = ""..(needed-have) + return nil + end +end + +function send_message(remote,opcode,data) + local out = string.char(0x80 + opcode) + local len = data:len() + if len > 65535 then + out = out .. string.char(127) + for i = 56,0,-8 do + out = out .. string.char(bit.band(bit.rshift(len, i),0xFF)) + end + elseif len > 125 then + out = out .. string.char(126) + out = out .. string.char(bit.rshift(len, 8)) .. string.char(bit.band(len,0xFF)) + else + out = out .. string.char(len) + end + out = out .. data + remote.client:send(out) + return nil +end + +function handle_websocket_complete_payload(source,data) + if source.ws.opcode == 0x09 then + -- ping -> pong + send_message(source,0x0A,data) + elseif source.ws.opcode == 0x02 or source.ws.opcode == 0x01 then + local status, err = pcall(function() + local response = handle_command(data) + if response then send_message(source,0x01,response.."\n") end + end) + return err + end + + return nil +end + +function handle_websocket_payload(source,data) + data = complete_data(source,data) + if data == nil then return nil end + local mask = data:sub(1,4) + local decoded = "" + for i = 1,source.ws.payload_len do + decoded = decoded .. string.char(bit.bxor( mask:byte( ((i-1)%4) + 1 ), data:byte(4+i) )) + end + + if source.ws.payload then + decoded = source.ws.payload .. decoded + end + + source.read_mode = "2" + source.handler = handle_websocket_frame + + if source.ws.frame_end then + source.ws.payload = nil + return handle_websocket_complete_payload(source,decoded) + else + source.ws.payload = decoded + end + + return nil +end + +function handle_websocket_payload_len(source,data) + data = complete_data(source,data) + if data == nil then return nil end + if source.read_mode == "1" then + source.ws.payload_len = data:byte(1) + elseif source.read_mode == "2" then + source.ws.payload_len = bit.lshift(data:byte(1),8)+data:byte(2) + elseif source.read_mode == "8" then + source.ws.payload_len = + bit.lshift(data:byte(1),56)+ + bit.lshift(data:byte(2),48)+ + bit.lshift(data:byte(3),40)+ + bit.lshift(data:byte(4),32)+ + bit.lshift(data:byte(5),24)+ + bit.lshift(data:byte(6),16)+ + bit.lshift(data:byte(7),8)+ + data:byte(8) + end + source.read_mode = ""..(4+source.ws.payload_len) + source.handler = handle_websocket_payload + return nil +end + +function handle_websocket_frame(source,data) + data = complete_data(source,data) + if data == nil then return nil end + local x = data:byte(1) + source.ws.frame_end = (0 ~= bit.band(0x80, x)) + local opcode = bit.band(0xF, x) + if opcode == 0x08 then + return "closed" + end + if opcode ~= 0 then + source.ws.opcode = opcode + end + local y = data:byte(2) + source.ws.mask = (0 ~= bit.band(0x80, y)) + if not source.ws.mask and opcode <= 0x02 then + return "unmasked data ("..opcode..")" + end + local payload_len = bit.band(y, 0x7F) + if payload_len == 126 then + source.handler = handle_websocket_payload_len + source.read_mode = "2" + elseif payload_len == 127 then + source.handler = handle_websocket_payload_len + source.read_mode = "8" + else + source.read_mode = "1" + return handle_websocket_payload_len(source,string.char(payload_len)) + end + return nil +end + +function handle_websocket_header(source,line) + if line:find("^Upgrade%: +websocket") then + source.ws.isWebsocket = true + return nil + end + + if not source.ws.key then + source.ws.key = line:match("^Sec%-WebSocket%-Key%: +([=+/0-9A-Za-z]+)") + end + + if line == "" then + if source.ws.isWebsocket and source.ws.key then + local new_key = tools.base64.encode(tools.sha1(source.ws.key .. '258EAFA5-E914-47DA-95CA-C5AB0DC85B11')) + source.ws = {frame=""} + local response = "HTTP/1.1 101 Switching Protocols\r\n".. + "Upgrade: websocket\r\n".. + "Connection: Upgrade\r\n".. + "Sec-WebSocket-Accept: "..new_key.."\r\n\r\n"; + source.client:send(response) + source.read_mode = "2" + source.handler = handle_websocket_frame + minetest.log("action","Websocket handshake") + return nil + else + return "invalid websocket request" + end + end + + return nil +end + -- TODO: test multiplayer functionality + diff --git a/raspberryjammod/settings.conf b/raspberryjammod/settings.conf index 652c0e8..33c3fa6 100755 --- a/raspberryjammod/settings.conf +++ b/raspberryjammod/settings.conf @@ -1,2 +1,3 @@ python = python local_only = false +support_websockets = true diff --git a/raspberryjammod/tools.lua b/raspberryjammod/tools.lua new file mode 100644 index 0000000..fbcf1fe --- /dev/null +++ b/raspberryjammod/tools.lua @@ -0,0 +1,203 @@ +local bit = require 'bit' +local rol = bit.rol +local bxor = bit.bxor +local bor = bit.bor +local band = bit.band +local bnot = bit.bnot +local lshift = bit.lshift +local rshift = bit.rshift +local sunpack = string.unpack +local srep = string.rep +local schar = string.char +local tremove = table.remove +local tinsert = table.insert +local tconcat = table.concat +local mrandom = math.random + +local read_n_bytes = function(str, pos, n) + pos = pos or 1 + return pos+n, string.byte(str, pos, pos + n - 1) +end + +local read_int8 = function(str, pos) + return read_n_bytes(str, pos, 1) +end + +local read_int16 = function(str, pos) + local new_pos,a,b = read_n_bytes(str, pos, 2) + return new_pos, lshift(a, 8) + b +end + +local read_int32 = function(str, pos) + local new_pos,a,b,c,d = read_n_bytes(str, pos, 4) + return new_pos, + lshift(a, 24) + + lshift(b, 16) + + lshift(c, 8 ) + + d +end + +local pack_bytes = string.char + +local write_int8 = pack_bytes + +local write_int16 = function(v) + return pack_bytes(rshift(v, 8), band(v, 0xFF)) +end + +local write_int32 = function(v) + return pack_bytes( + band(rshift(v, 24), 0xFF), + band(rshift(v, 16), 0xFF), + band(rshift(v, 8), 0xFF), + band(v, 0xFF) + ) +end + +-- used for generate key random ops +math.randomseed(os.time()) + +-- SHA1 hashing from luacrypto, if available +local sha1_crypto +local done,crypto = pcall(require,'crypto') +if done then + sha1_crypto = function(msg) + return crypto.digest('sha1',msg,true) + end +end + +-- from wiki article, not particularly clever impl +local sha1_wiki = function(msg) + local h0 = 0x67452301 + local h1 = 0xEFCDAB89 + local h2 = 0x98BADCFE + local h3 = 0x10325476 + local h4 = 0xC3D2E1F0 + + local bits = #msg * 8 + -- append b10000000 + msg = msg..schar(0x80) + + -- 64 bit length will be appended + local bytes = #msg + 8 + + -- 512 bit append stuff + local fill_bytes = 64 - (bytes % 64) + if fill_bytes ~= 64 then + msg = msg..srep(schar(0),fill_bytes) + end + + -- append 64 big endian length + local high = math.floor(bits/2^32) + local low = bits - high*2^32 + msg = msg..write_int32(high)..write_int32(low) + + assert(#msg % 64 == 0,#msg % 64) + + for j=1,#msg,64 do + local chunk = msg:sub(j,j+63) + assert(#chunk==64,#chunk) + local words = {} + local next = 1 + local word + repeat + next,word = read_int32(chunk, next) + tinsert(words, word) + until next > 64 + assert(#words==16) + for i=17,80 do + words[i] = bxor(words[i-3],words[i-8],words[i-14],words[i-16]) + words[i] = rol(words[i],1) + end + local a = h0 + local b = h1 + local c = h2 + local d = h3 + local e = h4 + + for i=1,80 do + local k,f + if i > 0 and i < 21 then + f = bor(band(b,c),band(bnot(b),d)) + k = 0x5A827999 + elseif i > 20 and i < 41 then + f = bxor(b,c,d) + k = 0x6ED9EBA1 + elseif i > 40 and i < 61 then + f = bor(band(b,c),band(b,d),band(c,d)) + k = 0x8F1BBCDC + elseif i > 60 and i < 81 then + f = bxor(b,c,d) + k = 0xCA62C1D6 + end + + local temp = rol(a,5) + f + e + k + words[i] + e = d + d = c + c = rol(b,30) + b = a + a = temp + end + + h0 = h0 + a + h1 = h1 + b + h2 = h2 + c + h3 = h3 + d + h4 = h4 + e + + end + + -- necessary on sizeof(int) == 32 machines + h0 = band(h0,0xffffffff) + h1 = band(h1,0xffffffff) + h2 = band(h2,0xffffffff) + h3 = band(h3,0xffffffff) + h4 = band(h4,0xffffffff) + + return write_int32(h0)..write_int32(h1)..write_int32(h2)..write_int32(h3)..write_int32(h4) +end + +local base64_encode = function(data) + local mime = require'mime' + return (mime.b64(data)) +end + +local DEFAULT_PORTS = {ws = 80, wss = 443} + +local parse_url = function(url) + local protocol, address, uri = url:match('^(%w+)://([^/]+)(.*)$') + if not protocol then error('Invalid URL:'..url) end + protocol = protocol:lower() + local host, port = address:match("^(.+):(%d+)$") + if not host then + host = address + port = DEFAULT_PORTS[protocol] + end + if not uri or uri == '' then uri = '/' end + return protocol, host, tonumber(port), uri +end + +local generate_key = function() + local r1 = mrandom(0,0xfffffff) + local r2 = mrandom(0,0xfffffff) + local r3 = mrandom(0,0xfffffff) + local r4 = mrandom(0,0xfffffff) + local key = write_int32(r1)..write_int32(r2)..write_int32(r3)..write_int32(r4) + assert(#key==16,#key) + return base64_encode(key) +end + +return { + sha1 = sha1_crypto or sha1_wiki, + base64 = { + encode = base64_encode + }, + parse_url = parse_url, + generate_key = generate_key, + read_int8 = read_int8, + read_int16 = read_int16, + read_int32 = read_int32, + write_int8 = write_int8, + write_int16 = write_int16, + write_int32 = write_int32, +}