├── 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 | [](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 |
--------------------------------------------------------------------------------