├── README.md ├── ajaxsocketconn.lua ├── digest.lua ├── distinfo ├── gettimeofday.lua ├── server.lua ├── simpleconn.lua ├── src ├── Makefile └── gettimeofday.c ├── websocketconn.lua ├── websockethixieconn.lua └── www └── websocket.js /README.md: -------------------------------------------------------------------------------- 1 | ## WebSocket Server for Lua 2 | 3 | [![Donate via Stripe](https://img.shields.io/badge/Donate-Stripe-green.svg)](https://buy.stripe.com/00gbJZ0OdcNs9zi288)
4 | 5 | Uses the following: 6 | 7 | - LuaSocket 8 | - LuaCrypto or LuaOSSL 9 | - LuaBitOp 10 | - dkjson 11 | - my Lua ext library 12 | - my ThreadManager library 13 | 14 | Timer is currently based on gettimeofday, which is loaded via LuaJIT's FFI, 15 | but if you wish to use non-FFI Lua you can use the `l_gettimeofday` lua binding function 16 | (or even `os.clock` or `os.time` if you don't mind the resolution). 17 | 18 | Also provides an AJAX fallback, but some specific client code must be used with this. 19 | 20 | To recieve messages, override the Server class 21 | Ex: 22 | ``` Lua 23 | local MyServer = class(require 'websocket.server') 24 | ``` 25 | 26 | To send messages, override the SimpleConn class and assign the new class to the server's connClass member. 27 | Ex: 28 | ``` Lua 29 | local MyConn = class(require 'websocket.simpleconn') 30 | MyServer.connClass = MyConn 31 | ``` 32 | 33 | requires the `LUA_CPATH` to include the folder where websocket is installed. I'm sure I will need to change the rockspec. 34 | -------------------------------------------------------------------------------- /ajaxsocketconn.lua: -------------------------------------------------------------------------------- 1 | local table = require 'ext.table' 2 | local class = require 'ext.class' 3 | local getTime = require 'websocket.gettimeofday' 4 | 5 | 6 | local AjaxSocketConn = class() 7 | 8 | AjaxSocketConn.timeout = 60 -- how long to timeout? 9 | 10 | --[[ 11 | args: 12 | server 13 | sessionID 14 | received = function(socketimpl, msg) 15 | --]] 16 | function AjaxSocketConn:init(args) 17 | -- no point in keeping track fo sockets -- they will change each ajax connect 18 | self.server = assert(args.server) 19 | self.sessionID = assert(args.sessionID) 20 | self.received = assert(args.received) 21 | 22 | self.sendQueue = table() 23 | self.lastPollTime = getTime() 24 | end 25 | 26 | function AjaxSocketConn:isActive() 27 | return getTime() - self.lastPollTime < self.timeout 28 | end 29 | 30 | function AjaxSocketConn:send(msg) 31 | --print('ajax sending size',#msg) 32 | self.sendQueue:insert(msg) 33 | end 34 | 35 | AjaxSocketConn.messageMaxLen = 512000 36 | 37 | function AjaxSocketConn:poll(receiveQueue) 38 | -- update timestamp 39 | self.lastPollTime = getTime() 40 | 41 | -- first read the messages from the headers 42 | -- call self:received(msg) on each 43 | for _,msg in ipairs(receiveQueue) do 44 | self:received(msg) 45 | end 46 | 47 | -- next send the new stuff out 48 | local sendQueue = table() 49 | local sendQueueSize = 0 50 | local partialPrefix = '(partial) ' 51 | local partialEndPrefix = '(partialEnd) ' 52 | while #self.sendQueue > 0 do 53 | local msg = self.sendQueue:remove(1) 54 | local remainingSize = self.messageMaxLen - sendQueueSize 55 | if #msg < self.messageMaxLen - sendQueueSize then 56 | -- granted this neglects json encoding data 57 | -- so lots of little values will throw it off 58 | sendQueueSize = sendQueueSize + #msg 59 | sendQueue:insert(msg) 60 | else 61 | -- might get multiple partialEnd's for multiply split msgs... 62 | if msg:sub(1,#partialEndPrefix) == partialEndPrefix then 63 | msg = msg:sub(#partialEndPrefix+1) 64 | end 65 | -- now send what we can and save the rest for later 66 | local partA = msg:sub(1,remainingSize) 67 | local partB = msg:sub(remainingSize+1) 68 | sendQueue:insert(partialPrefix..partA) 69 | self.sendQueue:insert(1, partialEndPrefix..partB) 70 | break 71 | end 72 | end 73 | 74 | return sendQueue 75 | end 76 | 77 | -- public, abstract 78 | function AjaxSocketConn:received(cmd) 79 | print("todo implement me: ",cmd) 80 | end 81 | 82 | -- called by the server? when the conn is to go down? 83 | -- does nothing ? 84 | function AjaxSocketConn:close() 85 | end 86 | 87 | return AjaxSocketConn 88 | -------------------------------------------------------------------------------- /digest.lua: -------------------------------------------------------------------------------- 1 | local digest 2 | 3 | local has, crypto = pcall(require,'crypto') -- luacrypto 4 | if has then 5 | digest = crypto.digest 6 | end 7 | if not digest then 8 | local has, openssl_digest = pcall(require,'openssl.digest') -- luaossl 9 | if has then 10 | local function bin2hex(c) return ('%02x'):format(c:byte()) end 11 | digest = function(algo, str, bin) 12 | local result = openssl_digest.new(algo):final(str) 13 | if not bin then result = result:gsub('.', bin2hex) end 14 | return result 15 | end 16 | end 17 | end 18 | if not digest then 19 | error("couldn't find a digest function") 20 | end 21 | return digest 22 | -------------------------------------------------------------------------------- /distinfo: -------------------------------------------------------------------------------- 1 | name = "websocket" 2 | files = { 3 | ["README.md"] = "websocket/README.md", 4 | ["ajaxsocketconn.lua"] = "websocket/ajaxsocketconn.lua", 5 | ["digest.lua"] = "websocket/digest.lua", 6 | ["gettimeofday.lua"] = "websocket/gettimeofday.lua", 7 | ["server.lua"] = "websocket/server.lua", 8 | ["simpleconn.lua"] = "websocket/simpleconn.lua", 9 | ["src/Makefile"] = "websocket/src/Makefile", 10 | ["src/gettimeofday.c"] = "websocket/src/gettimeofday.c", 11 | ["websocketconn.lua"] = "websocket/websocketconn.lua", 12 | ["websockethixieconn.lua"] = "websocket/websockethixieconn.lua", 13 | ["www/websocket.js"] = "websocket/www/websocket.js", 14 | } 15 | deps = { 16 | "ext", 17 | "threadmanager", 18 | } 19 | -------------------------------------------------------------------------------- /gettimeofday.lua: -------------------------------------------------------------------------------- 1 | -- [=[ using luajit ffi 2 | local success, getTime = pcall(function() 3 | local ffi = require 'ffi' 4 | local gettimeofday_tv 5 | return function() 6 | if not gettimeofday_tv then 7 | ffi.cdef[[ 8 | typedef long time_t; 9 | struct timeval { 10 | time_t tv_sec; 11 | time_t tv_usec; 12 | }; 13 | 14 | int gettimeofday(struct timeval*, void*); 15 | ]] 16 | gettimeofday_tv = ffi.new('struct timeval') 17 | end 18 | local results = ffi.C.gettimeofday(gettimeofday_tv, nil) 19 | return tonumber(gettimeofday_tv.tv_sec) + tonumber(gettimeofday_tv.tv_usec) / 1000000 20 | end 21 | end) 22 | --print(success, getTime) 23 | if success then 24 | --print('using ffi gettimeofday') 25 | return getTime 26 | end 27 | --]=] 28 | 29 | -- [=[ using .so 30 | local success, getTime = pcall(function() 31 | -- lua cpath needs to match path 32 | local l_gettimeofday = require 'websocket.lib.gettimeofday' 33 | return function() 34 | local sec, usec = l_gettimeofday() 35 | return sec + usec / 1000000 36 | end 37 | end) 38 | --print(success, getTime) 39 | if success then 40 | --print('using l_gettimeofday') 41 | return getTime 42 | end 43 | --]=] 44 | 45 | --print'using default os.clock' 46 | return os.clock 47 | -------------------------------------------------------------------------------- /server.lua: -------------------------------------------------------------------------------- 1 | local table = require 'ext.table' 2 | local class = require 'ext.class' 3 | local path = require 'ext.path' 4 | local socket = require 'socket' 5 | local mime = require 'mime' 6 | local json = require 'dkjson' 7 | local ThreadManager = require 'threadmanager' 8 | local WebSocketConn = require 'websocket.websocketconn' 9 | local WebSocketHixieConn = require 'websocket.websockethixieconn' 10 | local AjaxSocketConn = require 'websocket.ajaxsocketconn' 11 | local digest = require 'websocket.digest' 12 | 13 | local result 14 | local bit = bit32 15 | if not bit then 16 | result, bit = pcall(require, 'bit32') 17 | end 18 | if not bit then 19 | result, bit = pcall(require, 'bit') 20 | end 21 | 22 | 23 | 24 | local Server = class() 25 | 26 | -- used for indexing conns, and mapping the Server.conns table keys 27 | Server.nextConnUID = 1 28 | 29 | -- class for instanciation of connections 30 | Server.connClass = require 'websocket.simpleconn' 31 | 32 | -- default port goes here 33 | Server.port = 27000 34 | 35 | -- whether to use TLS 36 | Server.usetls = false 37 | 38 | -- websocket size limit before fragmentation. default = nil = use websocketconn class limit = infinite 39 | Server.sizeLimitBeforeFragmenting = nil 40 | 41 | -- how big the fragments should be. default = nil = use class default. 42 | Server.fragmentSize = nil 43 | 44 | --[[ 45 | args: 46 | hostname - to be sent back via socket header 47 | threads = (optional) ThreadManager. if you provide one then you have to update it manually. 48 | address (default is *) 49 | port (default is 27000) 50 | getTime (optional) = fraction-of-seconds-accurate timer function. default requires either FFI or an external C binding or os.clock ... or you can provide your own. 51 | keyfile = ssl key file 52 | certfile = ssl cert file 53 | - if keyfile and certfile are set then usetls will be used 54 | --]] 55 | function Server:init(args) 56 | args = args or {} 57 | self.port = args.port 58 | if args.keyfile and args.certfile then 59 | self.usetls = true 60 | self.keyfile = args.keyfile 61 | self.certfile = args.certfile 62 | end 63 | 64 | self.getTime = args.getTime or require 'websocket.gettimeofday' 65 | 66 | self.conns = table() 67 | self.ajaxConns = table() -- mapped from sessionID 68 | 69 | self.threads = args.threads 70 | if not self.threads then 71 | self.threads = ThreadManager() 72 | self.ownThreads = true 73 | end 74 | 75 | local address = args.address or '*' 76 | self.hostname = assert(args.hostname, "expected hostname") 77 | --DEBUG:self:log("hostname "..tostring(self.hostname)) 78 | --DEBUG:self:log("binding to "..tostring(address)..":"..tostring(self.port)) 79 | self.socket = assert(socket.bind(address, self.port)) 80 | self.socketaddr, self.socketport = self.socket:getsockname() 81 | --DEBUG:self:log('listening '..self.socketaddr..':'..self.socketport) 82 | self.socket:settimeout(0, 'b') 83 | end 84 | 85 | function Server:getNextConnUID() 86 | local uid = self.nextConnUID 87 | self.nextConnUID = self.nextConnUID + 1 88 | return uid 89 | end 90 | 91 | function Server:fmtTime() 92 | local f = self.getTime() 93 | local i = math.floor(f) 94 | return os.date('%Y/%m/%d %H:%M:%S', os.time())..(('%.3f'):format(f-i):match('%.%d+') or '') 95 | end 96 | 97 | function Server:log(...) 98 | print(self:fmtTime(), ...) 99 | end 100 | 101 | 102 | -- coroutine function that blocks til it gets something 103 | function Server:receiveBlocking(conn, waitduration) 104 | coroutine.yield() 105 | 106 | local endtime 107 | if waitduration then 108 | endtime = self.getTime() + waitduration 109 | end 110 | local data 111 | repeat 112 | coroutine.yield() 113 | local reason 114 | data, reason = conn:receive('*l') 115 | if not data then 116 | if reason == 'wantread' then 117 | --DEBUG:self:log('got wantread, calling select...') 118 | socket.select(nil, {conn}) 119 | --DEBUG:self:log('...done calling select') 120 | else 121 | if reason ~= 'timeout' then 122 | return nil, reason -- error() ? 123 | end 124 | -- else continue 125 | if waitduration and self.getTime() > endtime then 126 | return nil, 'timeout' 127 | end 128 | end 129 | end 130 | until data ~= nil 131 | 132 | return data 133 | end 134 | 135 | function Server:mustReceiveBlocking(conn, waitduration) 136 | local recv, reason = self:receiveBlocking(conn, waitduration) 137 | if not recv then error("Server waiting for handshake receive failed with error "..tostring(reason)) end 138 | return recv 139 | end 140 | 141 | -- send and make sure you send everything, and error upon fail 142 | function Server:send(conn, data) 143 | --DEBUG:self:log(conn, '<<', data) 144 | local i = 1 145 | while true do 146 | -- conn:send() successful response will be numberBytesSent, nil, nil, time 147 | -- conn:send() failed response will be nil, 'wantwrite', numBytesSent, time 148 | --DEBUG:self:log(conn, ' sending from '..i) 149 | local successlen, reason, faillen, time = conn:send(data:sub(i)) -- socket.send lets you use i,j as substring args, but does luasec's ssl.wrap ? 150 | --DEBUG:self:log(conn, '...', successlen, reason, faillen, time) 151 | --DEBUG:self:log(conn, '...getstats()', conn:getstats()) 152 | if successlen ~= nil then 153 | assert(reason ~= 'wantwrite') -- will wantwrite get set only if res[1] is nil? 154 | --DEBUG:self:log(conn, '...done sending') 155 | return successlen, reason, faillen, time 156 | end 157 | if reason ~= 'wantwrite' then 158 | error('socket.send failed: '..tostring(reason)) 159 | end 160 | --socket.select({conn}, nil) -- not good? 161 | -- try again 162 | i = i + faillen 163 | end 164 | end 165 | 166 | function Server:update() 167 | socket.sleep(.001) 168 | 169 | -- listen for new connections 170 | local client = self.socket:accept() 171 | if client then 172 | --DEBUG:self:log('got connection!',client) 173 | --DEBUG:self:log('connection from', client:getpeername()) 174 | --DEBUG:self:log('spawning new thread...') 175 | self.threads:add(self.connectRemoteCoroutine, self, client) 176 | end 177 | 178 | -- now handle connections 179 | for i,conn in pairs(self.conns) do 180 | if not conn:isActive() then 181 | -- only remove conns here ... using the following ... 182 | if conn.onRemove then 183 | conn:onRemove() 184 | end 185 | if AjaxSocketConn:isa(conn.socketImpl) then 186 | if self.ajaxConns[conn.socketImpl.sessionID] ~= conn then 187 | -- or todo, dump all conns here? 188 | --DEBUG:self:log('session', conn.socketImpl.sessionID, 'overwriting old conn', conn, 'with', self.ajaxConns[conn.socketImpl.sessionID]) 189 | else 190 | self.ajaxConns[conn.socketImpl.sessionID] = nil 191 | --DEBUG:self:log('removing ajax conn',conn.socketImpl.sessionID) 192 | end 193 | else 194 | --DEBUG:self:log('removing websocket conn') 195 | end 196 | self.conns[i] = nil 197 | else 198 | if conn.update then 199 | conn:update() 200 | end 201 | end 202 | end 203 | 204 | if self.ownThreads then 205 | self.threads:update() 206 | end 207 | end 208 | 209 | -- run loop 210 | function Server:run() 211 | xpcall(function() 212 | while not self.done do 213 | self:update() 214 | end 215 | 216 | for _,conn in pairs(self.conns) do 217 | if conn.onRemove then 218 | conn:onRemove() 219 | end 220 | conn:close() -- TODO should this be before onRemove() ? 221 | end 222 | end, function(err) 223 | self:traceback(err) 224 | end) 225 | end 226 | 227 | function Server:traceback(err) 228 | if err then io.stderr:write(err..'\n') end 229 | io.stderr:write(debug.traceback()..'\n') 230 | 231 | -- and all other threads? 232 | for _,thread in ipairs(self.threads.threads) do 233 | io.stderr:write('\n') 234 | io.stderr:write(tostring(thread)..'\n') 235 | io.stderr:write(debug.traceback(thread)..'\n') 236 | end 237 | 238 | io.stderr:flush() 239 | end 240 | 241 | function Server:delay(duration, callback, ...) 242 | local args = table.pack(...) 243 | local callingTrace = debug.traceback() 244 | self.threads:add(function() 245 | coroutine.yield() 246 | local thisTime = self.getTime() 247 | local startTime = thisTime 248 | local endTime = thisTime + duration 249 | repeat 250 | coroutine.yield() 251 | thisTime = self.getTime() 252 | until thisTime > endTime 253 | xpcall(function() 254 | callback(args:unpack()) 255 | end, function(err) 256 | io.stderr:write(tostring(err)..'\n') 257 | io.stderr:write(debug.traceback()) 258 | io.stderr:write(callingTrace) 259 | end) 260 | end) 261 | end 262 | 263 | local function be32ToStr(n) 264 | local s = '' 265 | for i=1,4 do 266 | s = string.char(bit.band(n, 0xff)) .. s 267 | n = bit.rshift(n, 8) 268 | end 269 | return s 270 | end 271 | 272 | -- create a remote connection 273 | function Server:connectRemoteCoroutine(client) 274 | 275 | -- do I have to do this for the tls before wrapping the tls? 276 | -- or can I only do this in the non-tls branch? 277 | client:setoption('keepalive', true) 278 | client:settimeout(0, 'b') -- for the benefit of coroutines ... 279 | 280 | -- TODO all of this should be in the client handle coroutine 281 | -- [[ can I do this? 282 | -- from https://stackoverflow.com/questions/2833947/stuck-with-luasec-lua-secure-socket 283 | -- TODO need to specify cert files 284 | -- TODO but if you want to handle both https and non-https on different ports, that means two connections, that means better make non-blocking the default 285 | if self.usetls then 286 | --DEBUG:self:log('upgrading to ssl...') 287 | local ssl = require 'ssl' -- package luasec 288 | -- TODO instead, just ask whoever is launching the server 289 | --DEBUG:self:log('keyfile', self.keyfile, 'exists', path(self.keyfile):exists()) 290 | --DEBUG:self:log('certfile', self.certfile, 'exists', path(self.certfile):exists()) 291 | assert(path(self.keyfile):exists()) 292 | assert(path(self.certfile):exists()) 293 | local err 294 | client, err = assert(ssl.wrap(client, { 295 | mode = 'server', 296 | options = {'all'}, 297 | protocol = 'any', 298 | -- luasec 0.6: 299 | -- following: https://github.com/brunoos/luasec/blob/master/src/https.lua 300 | --protocol = 'all', 301 | --options = {'all', 'no_sslv2', 'no_sslv3', 'no_tlsv1'}, 302 | key = self.keyfile, 303 | certificate = self.certfile, 304 | password = '12345', 305 | ciphers = 'ALL:!ADH:@STRENGTH', 306 | })) 307 | assert(client:settimeout(0, 'b')) 308 | --client:setkeepalive() -- nope 309 | --client:setoption('keepalive', true) -- nope 310 | --DEBUG:self:log('ssl.wrap error:', err) 311 | --DEBUG:self:log('doing handshake...') 312 | -- from https://github-wiki-see.page/m/brunoos/luasec/wiki/LuaSec-1.0.x 313 | -- also goes in receiveBlocking for conn:receive 314 | local result,reason 315 | while not result do 316 | coroutine.yield() 317 | result, reason = client:dohandshake() 318 | -- there can be a lot of these ... 319 | --DEBUG:self:log('dohandshake', result, reason) 320 | if reason == 'wantread' then 321 | --DEBUG:self:log('got wantread, calling select...') 322 | socket.select(nil, {client}) 323 | --DEBUG:self:log('...done calling select') 324 | end 325 | if reason == 'unknown state' then error('handshake conn in unknown state') end 326 | end 327 | --DEBUG:self:log("dohandshake finished") 328 | end 329 | --]] 330 | 331 | -- chrome has a bug where it connects and asks for a favicon even if there is none, or something, idk ... 332 | local firstLine, reason = self:receiveBlocking(client, 5) 333 | --DEBUG:self:log(client,'>>',firstLine,reason) 334 | if not (firstLine == 'GET / HTTP/1.1' or firstLine == 'POST / HTTP/1.1') then 335 | --DEBUG:self:log('got a non-http conn: ',firstLine) 336 | return 337 | end 338 | 339 | local header = table() 340 | while true do 341 | local recv = self:mustReceiveBlocking(client, 1) 342 | --DEBUG:self:log(client,'>>',recv) 343 | if recv == '' then break end 344 | local k,v = recv:match('^(.-): (.*)$') 345 | k = k:lower() 346 | header[k] = v 347 | end 348 | -- TODO make sure you got the right keys 349 | 350 | local cookies = table() 351 | if header.cookie then 352 | for kv in header.cookie:gmatch('(.-);%s?') do 353 | local k,v = kv:match('(.-)=(.*)') 354 | cookies[k] = v 355 | end 356 | end 357 | 358 | -- handle websockets 359 | -- IE doesn't give back an 'upgrade' 360 | if header.upgrade and header.upgrade:lower() == 'websocket' then 361 | 362 | local key1 = header['sec-websocket-key1'] 363 | local key2 = header['sec-websocket-key2'] 364 | if key1 and key2 then 365 | -- Hixie websockets 366 | -- http://www.whatwg.org/specs/web-socket-protocol/ 367 | local spaces1 = select(2, key1:gsub(' ', '')) 368 | local spaces2 = select(2, key2:gsub(' ', '')) 369 | local digits1 = assert(tonumber((key1:gsub('%D', '')))) / spaces1 370 | local digits2 = assert(tonumber((key2:gsub('%D', '')))) / spaces2 371 | 372 | local body, err, partial = client:receive(tonumber(header['content-length']) or '*a') 373 | body = body or partial 374 | --DEBUG:self:log(client,'>>',body) 375 | assert(#body == 8) 376 | 377 | local response = digest('md5', be32ToStr(digits1) .. be32ToStr(digits2) .. body, true) 378 | 379 | for _,line in ipairs{ 380 | 'HTTP/1.1 101 WebSocket Protocol Handshake\r\n', 381 | 'Upgrade: WebSocket\r\n', 382 | 'Connection: Upgrade\r\n', 383 | 'Sec-WebSocket-Origin: http://'..self.hostname..'\r\n', 384 | 'Sec-WebSocket-Location: ws://'..self.hostname..':'..self.socketport..'/\r\n', 385 | 'Sec-WebSocket-Protocol: sample\r\n', 386 | '\r\n', 387 | response, 388 | } do 389 | self:send(client, line) 390 | end 391 | 392 | local serverConn = self.connClass{ 393 | server = self, 394 | socket = client, 395 | implClass = WebSocketHixieConn, 396 | } 397 | self.lastActiveConnTime = self.getTime() 398 | return 399 | else 400 | -- RFC websockets 401 | 402 | local key = header['sec-websocket-key'] 403 | local magic = key .. '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' 404 | local sha1response = digest('sha1', magic, true) 405 | local response = mime.b64(sha1response) 406 | 407 | for _,line in ipairs{ 408 | 'HTTP/1.1 101 Switching Protocols\r\n', 409 | 'Upgrade: websocket\r\n', 410 | 'Connection: Upgrade\r\n', 411 | 'Sec-WebSocket-Accept: '..response..'\r\n', 412 | '\r\n', 413 | } do 414 | self:send(client, line) 415 | end 416 | 417 | -- only add to Server.conns through *HERE* 418 | --DEBUG:self:log('creating websocket conn') 419 | local serverConn = self.connClass{ 420 | server = self, 421 | socket = client, 422 | implClass = WebSocketConn, 423 | sizeLimitBeforeFragmenting = self.sizeLimitBeforeFragmenting, 424 | fragmentSize = self.fragmentSize, 425 | } 426 | --DEBUG:self:log('constructing ServerConn',serverConn,'...') 427 | self.lastActiveConnTime = self.getTime() 428 | return 429 | end 430 | end 431 | 432 | 433 | -- handle ajax connections 434 | 435 | local serverConn 436 | 437 | local body, err, partial = client:receive(tonumber(header['content-length']) or '*a') 438 | body = body or partial 439 | --DEBUG:self:log(client,'>>',body) 440 | local receiveQueue = json.decode(body) 441 | local sessionID 442 | if not receiveQueue then 443 | --DEBUG:self:log('failed to decode ajax body',body) 444 | receiveQueue = {} 445 | else 446 | if #receiveQueue > 0 then 447 | local msg = receiveQueue[1] 448 | if msg:sub(1,10) == 'sessionID ' then 449 | table.remove(receiveQueue, 1) 450 | sessionID = msg:sub(11) 451 | end 452 | end 453 | end 454 | --DEBUG:self:log('got session id', sessionID) 455 | 456 | local newSessionID 457 | if sessionID then -- if the client has a sessionID then ... 458 | -- see if the server has an ajax connection wrapper waiting ... 459 | serverConn = self.ajaxConns[sessionID] 460 | -- these are fake conn objects -- they merge multiple conns into one polling fake conn 461 | -- so headers and data need to be re-sent every time a new poll conn is made 462 | if not serverConn then 463 | --DEBUG:self:log('NO CONN FOR ', sessionID) 464 | end 465 | else 466 | newSessionID = true 467 | sessionID = mime.b64(digest('sha1', header:values():concat()..os.date(), true)) 468 | --DEBUG:self:log('no sessionID -- generating session id', sessionID) 469 | end 470 | -- no pre-existing connection? make a new one 471 | if serverConn then 472 | --DEBUG:self:log('updating ajax conn') 473 | else 474 | --DEBUG:self:log('creating ajax conn',sessionID,newSessionID) 475 | serverConn = self.connClass{ 476 | server = self, 477 | implClass = AjaxSocketConn, 478 | sessionID = sessionID, 479 | } 480 | self.ajaxConns[sessionID] = serverConn 481 | self.lastActiveConnTime = self.getTime() 482 | end 483 | 484 | -- now hand it off to the serverConn to process sends & receives ... 485 | local responseQueue = serverConn.socketImpl:poll(receiveQueue) 486 | if newSessionID then 487 | table.insert(responseQueue, 1, 'sessionID '..sessionID) 488 | end 489 | local response = json.encode(responseQueue) 490 | 491 | --DEBUG:self:log('sending ajax response size',#response,'body',response) 492 | 493 | -- send response header 494 | local lines = table() 495 | lines:insert('HTTP/1.1 200 OK') 496 | --lines:insert('Date: '..os.date('!%a, %d %b %Y %T')..' GMT') 497 | lines:insert('content-type: text/plain') --droid4 default browser is mystery crashing... i suspect it cant handle json responses... 498 | 499 | -- when I use this I get an error in Chrome: net::ERR_CONTENT_LENGTH_MISMATCH 200 (OK) ... so just don't use this? 500 | -- when I don't use this I get a truncated json message 501 | lines:insert('content-length: '..#response..'') 502 | 503 | lines:insert('pragma: no-cache') 504 | lines:insert('cache-control: no-cache, no-store, must-revalidate') 505 | lines:insert('expires: 0') 506 | 507 | lines:insert('access-control-allow-origin: *') -- same url different port is considered cross-domain because reasons 508 | --lines:insert('Connection: close') -- IE needs this 509 | lines:insert('') 510 | lines:insert(response) 511 | 512 | -- [[ send all at once 513 | local msg = lines:concat'\r\n' 514 | self:send(client, msg) 515 | --]] 516 | --[[ send line by line 517 | for i,line in ipairs(lines) do 518 | self:send(client, line..(i < #lines and '\r\n' or '')) 519 | end 520 | --]] 521 | --[[ send chunk by chunk. does luasec or luasocket have a maximum send size? 522 | local msg = lines:concat'\r\n' 523 | local n = #msg 524 | local chunkSize = 4096 525 | for i=0,n-1,chunkSize do 526 | local len = math.min(chunkSize, n-i) 527 | local submsg = msg:sub(i+1, i+len) 528 | self:send(client, submsg) 529 | end 530 | --]] 531 | 532 | client:close() 533 | end 534 | 535 | return Server 536 | -------------------------------------------------------------------------------- /simpleconn.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | provides the basics of a conn class's interaction with both the server and the conn implementation 3 | --]] 4 | 5 | local class = require 'ext.class' 6 | local SimpleConn = class() 7 | 8 | function SimpleConn:init(args) 9 | args.received = function(impl, ...) 10 | return self:received(...) 11 | end 12 | self.socketImpl = args.implClass(args) 13 | 14 | self.server = args.server 15 | self.uid = self.server:getNextConnUID() 16 | self.server.conns[self.uid] = self 17 | end 18 | 19 | function SimpleConn:isActive(...) return self.socketImpl:isActive(...) end 20 | function SimpleConn:close(...) return self.socketImpl:close(...) end 21 | function SimpleConn:send(msg) self.socketImpl:send(msg) end 22 | function SimpleConn:received(data) 23 | print('received',data) 24 | end 25 | 26 | return SimpleConn 27 | -------------------------------------------------------------------------------- /src/Makefile: -------------------------------------------------------------------------------- 1 | CFLAGS=-O2 -fpic -I/usr/local/include/lua-5.3 2 | 3 | .PHONY: all 4 | all: gettimeofday.so 5 | 6 | gettimeofday.so: gettimeofday.o 7 | gcc -O -shared -fpic -o gettimeofday.so gettimeofday.o 8 | 9 | gettimeofday.o: 10 | gcc ${CFLAGS} -c -o gettimeofday.o gettimeofday.c 11 | 12 | .PHONY: clean 13 | clean: 14 | -rm gettimeofday.so 15 | -rm gettimeofday.o 16 | -------------------------------------------------------------------------------- /src/gettimeofday.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | int l_gettimeofday(lua_State *L) { 7 | struct timeval tv; 8 | gettimeofday(&tv, NULL); 9 | lua_pushnumber(L, tv.tv_sec); 10 | lua_pushnumber(L, tv.tv_usec); 11 | return 2; 12 | } 13 | 14 | int luaopen_websocket_lib_gettimeofday(lua_State *L) { 15 | lua_pushcfunction(L, l_gettimeofday); 16 | return 1; 17 | } 18 | -------------------------------------------------------------------------------- /websocketconn.lua: -------------------------------------------------------------------------------- 1 | local table = require 'ext.table' 2 | local class = require 'ext.class' 3 | local coroutine = require 'ext.coroutine' 4 | local socket = require 'socket' 5 | 6 | local result 7 | local bit = bit32 8 | if not bit then 9 | result, bit = pcall(require, 'bit32') 10 | end 11 | if not bit then 12 | result, bit = pcall(require, 'bit') 13 | end 14 | 15 | 16 | local WebSocketConn = class() 17 | 18 | WebSocketConn.sizeLimitBeforeFragmenting = math.huge 19 | WebSocketConn.fragmentSize = 100 20 | --WebSocketConn.fragmentSize = 1024 21 | --WebSocketConn.fragmentSize = 65535 -- NOTICE I don't have suport for >64k fragments yet 22 | 23 | --[[ 24 | args: 25 | server 26 | socket = luasocket 27 | received = function(socketimpl, msg) (optional) 28 | sizeLimitBeforeFragmenting (optional) 29 | fragmentSize (optional) 30 | --]] 31 | function WebSocketConn:init(args) 32 | self.server = assert(args.server) 33 | self.socket = assert(args.socket) 34 | self.received = args.received 35 | self.sizeLimitBeforeFragmenting = args.sizeLimitBeforeFragmenting 36 | self.fragmentSize = args.fragmentSize 37 | 38 | self.listenThread = self.server.threads:add(self.listenCoroutine, self) 39 | self.readFrameThread = coroutine.create(self.readFrameCoroutine) -- not part of the thread pool -- yielded manually with the next read byte 40 | coroutine.assertresume(self.readFrameThread, self) -- position us to wait for the first byte 41 | end 42 | 43 | -- public 44 | function WebSocketConn:isActive() 45 | return coroutine.status(self.listenThread) ~= 'dead' 46 | end 47 | 48 | -- private 49 | function WebSocketConn:readFrameCoroutine_DecodeData() 50 | local sizeByte = coroutine.yield() 51 | local useMask = bit.band(sizeByte, 0x80) ~= 0 52 | local size = bit.band(sizeByte, 0x7f) 53 | if size == 127 then 54 | size = 0 55 | for i=1,8 do 56 | size = bit.lshift(size, 8) 57 | size = bit.bor(size, coroutine.yield()) -- are lua bit ops 64 bit? well, bit32 is not for sure. 58 | end 59 | elseif size == 126 then 60 | size = 0 61 | for i=1,2 do 62 | size = bit.lshift(size, 8) 63 | size = bit.bor(size, coroutine.yield()) 64 | end 65 | end 66 | 67 | assert(useMask, "expected all received frames to use mask") 68 | if useMask then 69 | local mask = table() 70 | for i=1,4 do 71 | mask[i] = coroutine.yield() 72 | end 73 | 74 | local decoded = table() 75 | for i=1,size do 76 | decoded[i] = string.char(bit.bxor(mask[(i-1)%4+1], coroutine.yield())) 77 | end 78 | return decoded:concat() 79 | end 80 | end 81 | 82 | local FRAME_CONTINUE = 0 83 | local FRAME_TEXT = 1 84 | local FRAME_DATA = 2 85 | local FRAME_CLOSE = 8 86 | local FRAME_PING = 9 87 | local FRAME_PONG = 10 88 | 89 | -- private 90 | function WebSocketConn:readFrameCoroutine() 91 | while not self.done 92 | and self.server 93 | and self.server.socket:getsockname() 94 | do 95 | -- this is blocking ... 96 | local op = coroutine.yield() 97 | local fin = bit.band(op, 0x80) ~= 0 98 | local reserved1 = bit.band(op, 0x40) ~= 0 99 | local reserved2 = bit.band(op, 0x20) ~= 0 100 | local reserved3 = bit.band(op, 0x10) ~= 0 101 | local opcode = bit.band(op, 0xf) 102 | assert(not reserved1) 103 | assert(not reserved2) 104 | assert(not reserved3) 105 | assert(fin) -- TODO handle continuations 106 | if opcode == FRAME_CONTINUE then -- continuation frame 107 | error('readFrameCoroutine got continuation frame') -- TODO handle continuations 108 | elseif opcode == FRAME_TEXT or opcode == FRAME_DATA then -- new text/binary frame 109 | local decoded = self:readFrameCoroutine_DecodeData() 110 | --DEBUG:print('readFrameCoroutine got',decoded) 111 | -- now process 'decoded' 112 | self.server.threads:add(function() 113 | self:received(decoded) 114 | end) 115 | elseif opcode == FRAME_CLOSE then -- connection close 116 | --DEBUG:print('readFrameCoroutine got connection close') 117 | local decoded = self:readFrameCoroutine_DecodeData() 118 | --DEBUG:print('connection closed. reason ('..#decoded..'):',decoded) 119 | self.done = true 120 | break 121 | elseif opcode == FRAME_PING then -- ping 122 | --DEBUG:print('readFrameCoroutine got ping') 123 | -- TODO send FRAME_PONG response? 124 | elseif opcode == FRAME_PONG then -- pong 125 | --DEBUG:print('readFrameCoroutine got pong') 126 | else 127 | error("got a reserved opcode: "..opcode) 128 | end 129 | end 130 | --DEBUG:print('readFrameCoroutine stopped') 131 | end 132 | 133 | -- private 134 | function WebSocketConn:listenCoroutine() 135 | coroutine.yield() 136 | 137 | while not self.done 138 | and self.server 139 | and self.server.socket:getsockname() 140 | do 141 | local b, reason = self.socket:receive(1) 142 | if b then 143 | coroutine.assertresume(self.readFrameThread, b:byte()) 144 | else 145 | if reason == 'wantread' then 146 | -- luasec case 147 | --DEBUG:print('got wantread, calling select...') 148 | socket.select(nil, {self.socket}) 149 | --DEBUG:print('...done calling select') 150 | elseif reason == 'timeout' then 151 | elseif reason == 'closed' then 152 | self.done = true 153 | break 154 | else 155 | error(reason) 156 | end 157 | end 158 | -- TODO sleep 159 | 160 | coroutine.yield() 161 | end 162 | 163 | -- one thread needs responsibility to close ... 164 | self:close() 165 | --DEBUG:print('listenCoroutine stopped') 166 | end 167 | 168 | -- public 169 | function WebSocketConn:send(msg, opcode) 170 | if not opcode then 171 | opcode = FRAME_TEXT 172 | --opcode = FRAME_DATA 173 | end 174 | 175 | -- upon sending this to the browser, the browser is sending back 6 chars and then crapping out 176 | -- no warnings given. browser keeps acting like all is cool but it stops actually sending data afterwards. 177 | local nmsg = #msg 178 | --DEBUG:print('send',nmsg,msg) 179 | if nmsg < 126 then 180 | local data = string.char( 181 | bit.bor(0x80, opcode), 182 | nmsg) 183 | .. msg 184 | assert(#data == 2 + nmsg) 185 | 186 | self.server:send(self.socket, data) 187 | elseif nmsg >= self.sizeLimitBeforeFragmenting then 188 | -- multiple fragmented frames 189 | -- ... it looks like the browser is sending the fragment headers to websocket onmessage? along with the frame data? 190 | --DEBUG:print('sending large websocket frame fragmented -- msg size',nmsg) 191 | local fragopcode = opcode 192 | for start=0,nmsg-1,self.fragmentSize do 193 | local len = self.fragmentSize 194 | if start + len >= nmsg then len = nmsg - start end 195 | local headerbyte = fragopcode 196 | if start + len == nmsg then 197 | headerbyte = bit.bor(headerbyte, 0x80) 198 | end 199 | --DEBUG:print('sending header '..headerbyte..' len '..len) 200 | local data 201 | if len < 126 then 202 | data = string.char( 203 | headerbyte, 204 | len) 205 | .. msg:sub(start+1, start+len) 206 | assert(#data == 2 + len) 207 | self.server:send(self.socket, data) 208 | else 209 | assert(len < 65536) 210 | data = string.char( 211 | headerbyte, 212 | 126, 213 | bit.band(bit.rshift(len, 8), 0xff), 214 | bit.band(len, 0xff)) 215 | .. msg:sub(start+1, start+len) 216 | assert(#data == 4 + len) 217 | self.server:send(self.socket, data) 218 | end 219 | fragopcode = 0 220 | end 221 | 222 | elseif nmsg < 65536 then 223 | local data = string.char( 224 | bit.bor(0x80, opcode), 225 | 126, 226 | bit.band(bit.rshift(nmsg, 8), 0xff), 227 | bit.band(nmsg, 0xff)) 228 | .. msg 229 | assert(#data == 4 + nmsg) 230 | 231 | self.server:send(self.socket, data) 232 | 233 | else 234 | -- large frame ... not working? 235 | -- these work fine localhost / non-tls 236 | -- but when I use tls / Chrome doesn't seem to receive 237 | --DEBUG:print('sending large websocket frame of size', nmsg) 238 | local data = string.char( 239 | bit.bor(0x80, opcode), 240 | 127, 241 | --[[ luaresty's websockets limit size at 2gb ... 242 | bit.band(0xff, bit.rshift(nmsg, 56)), 243 | bit.band(0xff, bit.rshift(nmsg, 48)), 244 | bit.band(0xff, bit.rshift(nmsg, 40)), 245 | bit.band(0xff, bit.rshift(nmsg, 32)), 246 | --]] 247 | -- [[ 248 | 0,0,0,0, 249 | --]] 250 | bit.band(0xff, bit.rshift(nmsg, 24)), 251 | bit.band(0xff, bit.rshift(nmsg, 16)), 252 | bit.band(0xff, bit.rshift(nmsg, 8)), 253 | bit.band(0xff, nmsg)) 254 | .. msg 255 | assert(#data == 10 + nmsg) 256 | self.server:send(self.socket, data) 257 | --]] 258 | end 259 | end 260 | 261 | -- public, abstract 262 | function WebSocketConn:received(cmd) 263 | print("todo implement me: ",cmd) 264 | end 265 | 266 | -- public 267 | function WebSocketConn:close(reason) 268 | if reason == nil then reason = '' end 269 | reason = tostring(reason) 270 | self:send('goodbye', FRAME_CLOSE) 271 | self.socket:close() 272 | self.done = true 273 | end 274 | 275 | return WebSocketConn 276 | -------------------------------------------------------------------------------- /websockethixieconn.lua: -------------------------------------------------------------------------------- 1 | local table = require 'ext.table' 2 | local class = require 'ext.class' 3 | local getTime = require 'websocket.gettimeofday' 4 | 5 | 6 | local WebSocketHixieConn = class() 7 | 8 | --[[ 9 | args: 10 | server 11 | socket 12 | received = function(socketimpl, msg) 13 | --]] 14 | function WebSocketHixieConn:init(args) 15 | local sock = assert(args.socket) 16 | self.server = assert(args.server) 17 | self.socket = sock 18 | self.received = assert(args.received) 19 | 20 | self.listenThread = self.server.threads:add(self.listenCoroutine, self) 21 | self.readFrameThread = coroutine.create(self.readFrameCoroutine) -- not part of the thread pool -- yielded manually with the next read byte 22 | coroutine.resume(self.readFrameThread, self) -- position us to wait for the first byte 23 | end 24 | 25 | -- public 26 | function WebSocketHixieConn:isActive() 27 | return coroutine.status(self.listenThread) ~= 'dead' 28 | end 29 | 30 | -- private 31 | function WebSocketHixieConn:readFrameCoroutine() 32 | local data 33 | while not self.done and self.server and self.server.socket:getsockname() do 34 | local ch = coroutine.yield() 35 | if ch:byte() == 0 then -- begin 36 | data = table() 37 | elseif ch:byte() == 0xff then -- end 38 | data = data:concat() 39 | --print(getTime(),self.socket,'>>',data) 40 | self.server.threads:add(function() 41 | self:received(data) 42 | end) 43 | data = nil 44 | else 45 | assert(data, "recieved data outside of a frame") 46 | data:insert(ch) 47 | end 48 | end 49 | end 50 | 51 | -- private 52 | function WebSocketHixieConn:listenCoroutine() 53 | coroutine.yield() 54 | 55 | while not self.done 56 | and self.server 57 | and self.server.socket:getsockname() 58 | do 59 | local b, reason = self.socket:receive(1) 60 | if b then 61 | --print(getTime(),self.socket,'>>',('%02x'):format(b:byte())) 62 | local res, err = coroutine.resume(self.readFrameThread, b) 63 | if not res then 64 | error(err..'\n'..debug.traceback(self.readFrameThread)) 65 | end 66 | else 67 | if reason == 'timeout' then 68 | elseif reason == 'closed' then 69 | self.done = true 70 | break 71 | else 72 | error(reason) 73 | end 74 | end 75 | -- TODO sleep 76 | 77 | coroutine.yield() 78 | end 79 | 80 | -- one thread needs responsibility to close ... 81 | self:close() 82 | --print('listenCoroutine stopped') 83 | end 84 | 85 | -- public 86 | function WebSocketHixieConn:send(msg) 87 | print(getTime(),self.socket,'<<',msg) 88 | self.server:send(self.socket, string.char(0x00) .. msg .. string.char(0xff)) 89 | end 90 | 91 | -- public, abstract 92 | function WebSocketHixieConn:received(cmd) 93 | print("todo implement me: ",cmd) 94 | end 95 | 96 | -- public 97 | function WebSocketHixieConn:close(reason) 98 | if reason == nil then reason = '' end 99 | reason = tostring(reason) 100 | self:send(string.rep(string.char(0), 9)) 101 | self.socket:close() 102 | self.done = true 103 | end 104 | 105 | return WebSocketHixieConn 106 | -------------------------------------------------------------------------------- /www/websocket.js: -------------------------------------------------------------------------------- 1 | /* 2 | send messages via clientConn.send 3 | receive messages by providing an onMessage function 4 | 5 | requires my js-util project 6 | */ 7 | 8 | class ClientConn { 9 | constructor(args) { 10 | if (args.uri !== undefined) this.uri = args.uri; 11 | if (this.uri === undefined) throw "expected uri"; 12 | 13 | //ok seems firefox has a problem connecting non-wss to https domains ... i guess? 14 | // or is that the new standard now? 15 | // either way since I've switched to https only (and so many others have too ...) 16 | // default is wss 17 | if (args.wsProto !== undefined) this.wsProto = args.wsProto; 18 | if (this.wsProto === undefined) this.wsProto = 'wss'; 19 | 20 | if (args.ajaxProto !== undefined) this.ajaxProto = args.ajaxProto; 21 | if (this.ajaxProto === undefined) this.ajaxProto = 'https'; 22 | 23 | let clientConn = this; 24 | 25 | //callback on received message 26 | this.onMessage = args.onMessage; 27 | 28 | //callback on closed connection 29 | this.onClose = args.onClose; 30 | 31 | //buffer responses until we have a connection 32 | this.sendQueue = []; 33 | 34 | //register implementation classes 35 | class AsyncComm {}; 36 | this.AsyncComm = AsyncComm; 37 | 38 | class AsyncCommWebSocket extends AsyncComm { 39 | constructor(done) { 40 | super(); 41 | this.connected = false; 42 | this.reconnect(done); 43 | } 44 | reconnect(done) { 45 | if (this.connected) return; 46 | let thiz = this; 47 | this.ws = new WebSocket(clientConn.wsProto+'://'+clientConn.uri); 48 | this.ws.addEventListener('open', evt => { 49 | //console.log('websocket open', evt); 50 | thiz.connected = true; 51 | if (done) done(); 52 | }); 53 | this.ws.addEventListener('close', evt => { 54 | console.log('websocket onclose', evt); 55 | console.log('error code', evt.code); 56 | thiz.connected = false; 57 | if (clientConn.onClose) { 58 | clientConn.onClose.apply(clientConn, arguments); 59 | } 60 | }); 61 | this.ws.addEventListener('message', evt => { 62 | //console.log('websocket onmessage', evt); 63 | let isblob = evt.data.constructor == Blob; 64 | if (isblob) { 65 | // blob to text, because javascript is a trash language/API 66 | let reader = new FileReader(); 67 | reader.addEventListener('load', e => { 68 | let text = reader.result; 69 | clientConn.onMessage(text); 70 | }); 71 | reader.readAsText(evt.data); 72 | } else { 73 | // text ... I hope 74 | clientConn.onMessage(evt.data); 75 | } 76 | }); 77 | this.ws.addEventListener('error', evt => { 78 | console.log('websocket onerror', arguments); 79 | // https://stackoverflow.com/questions/18803971/websocket-onerror-how-to-read-error-description 80 | // optimistic but not standard .... and not showing up on chrome desktop 81 | //console.log('error code', evt.code); 82 | throw evt; 83 | }); 84 | } 85 | send(data) { 86 | this.ws.send(data); 87 | } 88 | } 89 | AsyncCommWebSocket.prototype.name = 'AsyncCommWebSocket'; 90 | this.AsyncCommWebSocket = AsyncCommWebSocket; 91 | 92 | class AsyncCommAjax extends AsyncComm { 93 | constructor(done) { 94 | super(); 95 | this.sessionID = undefined; 96 | this.connected = true; 97 | this.sendQueue = []; 98 | this.partialMsg = ''; 99 | this.poll(); 100 | if (done) done(); 101 | } 102 | reconnect() {} //nothing right now 103 | poll() { 104 | let thiz = this; 105 | setTimeout(function() { 106 | let sendQueue = thiz.sendQueue; 107 | //cookies cross domain? port change means domain change? just wrap sessions into the protocol ... 108 | if (thiz.sessionID !== undefined) { 109 | sendQueue.splice(0, 0, 'sessionID '+thiz.sessionID); 110 | } 111 | thiz.sendQueue = []; 112 | fetch( 113 | clientConn.ajaxProto+'://'+clientConn.uri, 114 | { 115 | body : JSON.stringify(sendQueue), 116 | }).then(response => { 117 | if (!response.ok) throw 'not ok'; 118 | console.log('got response',response); 119 | response.json(msgs => { 120 | console.log('got msgs',msgs); 121 | //process responses 122 | for (let i = 0; i < msgs.length; i++) { 123 | let msg = msgs[i]; 124 | if (msg.substring(0,10) == 'sessionID ') { 125 | //console.log("sessionID is", msg.substring(10)); 126 | thiz.sessionID = msg.substring(10); 127 | } else if (msg.substring(0,10) == '(partial) ') { 128 | let part = msg.substring(10); 129 | thiz.partialMsg += part; 130 | } else if (msg.substring(0,13) == '(partialEnd) ') { 131 | let part = msg.substring(13); 132 | let partialMsg = thiz.partialMsg + part; 133 | thiz.partialMsg = ''; 134 | clientConn.onMessage(partialMsg); 135 | } else { 136 | clientConn.onMessage(msgs[i]); 137 | } 138 | } 139 | thiz.poll(); 140 | }); 141 | }).catch(e => { 142 | console.log("fetch error", e); 143 | }); 144 | //timeout : 30000 145 | // ... does fetch have no timeout otherwise? 146 | // ... do I have to add 'AbortSignal.timeout(30000) to change the default? 147 | }, 500); 148 | } 149 | send(msg) { 150 | this.sendQueue.push(msg); 151 | } 152 | } 153 | AsyncCommAjax.prototype.name = 'AsyncCommAjax'; 154 | this.AsyncCommAjax = AsyncCommAjax; 155 | 156 | //first try websockets ... 157 | //mind you, the server only handles the RFC websockets 158 | this.commClasses = []; 159 | if (!args.disableWebsocket) this.commClasses.push(this.AsyncCommWebSocket); 160 | if (!args.disableAjax) this.commClasses.push(this.AsyncCommAjax); 161 | } 162 | 163 | connect(done) { 164 | this.impl = undefined; 165 | for (let i = 0; i < this.commClasses.length; i++) { 166 | try { 167 | //console.log("websocket comm attempting class", this.commClasses[i].prototype.name); 168 | this.impl = new this.commClasses[i](done); 169 | console.log('websocket comm succeeded with', this.commClasses[i].prototype.name); 170 | break; 171 | } catch (ex) { 172 | console.log('conn init failed',this.commClasses[i].prototype.name, ex); 173 | } 174 | } 175 | if (this.impl === undefined) throw 'failed to initialize any kind of async communication'; 176 | } 177 | 178 | send(msg) { 179 | if (!this.impl || !this.impl.connected) { 180 | this.sendQueue.push(msg); 181 | //TODO and register a loop to check 182 | } else { 183 | if (this.sendQueue.length) { 184 | let thiz = this; 185 | this.sendQueue.forEach(msg => { 186 | thiz.impl.send(msg); 187 | }); 188 | this.sendQueue = []; 189 | } 190 | this.impl.send(msg); 191 | } 192 | } 193 | } 194 | 195 | export {ClientConn}; 196 | --------------------------------------------------------------------------------