websocket support

This commit is contained in:
arpruss 2015-09-27 01:30:52 -05:00
parent eedfc34e6d
commit 0b203d8bca
3 changed files with 424 additions and 17 deletions

View File

@ -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

View File

@ -1,2 +1,3 @@
python = python
local_only = false
support_websockets = true

203
raspberryjammod/tools.lua Normal file
View 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,
}