websocket support
This commit is contained in:
parent
eedfc34e6d
commit
0b203d8bca
@ -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
|
||||
|
||||
|
@ -1,2 +1,3 @@
|
||||
python = python
|
||||
local_only = false
|
||||
support_websockets = true
|
||||
|
203
raspberryjammod/tools.lua
Normal file
203
raspberryjammod/tools.lua
Normal file
@ -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,
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user