]>
Commit | Line | Data |
---|---|---|
72f1348b JM |
1 | /* |
2 | * Websock: high-performance binary WebSockets | |
d58f8b51 | 3 | * Copyright (C) 2012 Joel Martin |
1d728ace | 4 | * Licensed under MPL 2.0 (see LICENSE.txt) |
72f1348b JM |
5 | * |
6 | * Websock is similar to the standard WebSocket object but Websock | |
7 | * enables communication with raw TCP sockets (i.e. the binary stream) | |
8 | * via websockify. This is accomplished by base64 encoding the data | |
9 | * stream between Websock and websockify. | |
10 | * | |
11 | * Websock has built-in receive queue buffering; the message event | |
12 | * does not contain actual data but is simply a notification that | |
13 | * there is new data available. Several rQ* methods are available to | |
14 | * read binary data off of the receive queue. | |
15 | */ | |
16 | ||
2cccf753 | 17 | /*jslint browser: true, bitwise: true */ |
ff4bfcb7 JM |
18 | /*global Util, Base64 */ |
19 | ||
72f1348b JM |
20 | |
21 | // Load Flash WebSocket emulator if needed | |
22 | ||
bee36506 | 23 | // To force WebSocket emulator even when native WebSocket available |
e5d5a7d3 | 24 | //window.WEB_SOCKET_FORCE_FLASH = true; |
bee36506 | 25 | // To enable WebSocket emulator debug: |
e5d5a7d3 | 26 | //window.WEB_SOCKET_DEBUG=1; |
bee36506 | 27 | |
f2d85676 | 28 | if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) { |
72f1348b | 29 | Websock_native = true; |
f2d85676 | 30 | } else if (window.MozWebSocket && !window.WEB_SOCKET_FORCE_FLASH) { |
ce3bdbcc JM |
31 | Websock_native = true; |
32 | window.WebSocket = window.MozWebSocket; | |
72f1348b JM |
33 | } else { |
34 | /* no builtin WebSocket so load web_socket.js */ | |
b688a909 | 35 | |
72f1348b JM |
36 | Websock_native = false; |
37 | (function () { | |
6f4b1e40 | 38 | window.WEB_SOCKET_SWF_LOCATION = Util.get_include_uri() + |
61fc1f22 | 39 | "web-socket-js/WebSocketMain.swf"; |
0981845e JM |
40 | if (Util.Engine.trident) { |
41 | Util.Debug("Forcing uncached load of WebSocketMain.swf"); | |
ff4bfcb7 | 42 | window.WEB_SOCKET_SWF_LOCATION += "?" + Math.random(); |
0981845e | 43 | } |
6f4b1e40 JM |
44 | Util.load_scripts(["web-socket-js/swfobject.js", |
45 | "web-socket-js/web_socket.js"]); | |
2cccf753 | 46 | })(); |
72f1348b JM |
47 | } |
48 | ||
49 | ||
50 | function Websock() { | |
2cccf753 SR |
51 | "use strict"; |
52 | ||
53 | this._websocket = null; // WebSocket object | |
54 | this._rQ = []; // Receive queue | |
55 | this._rQi = 0; // Receive queue index | |
56 | this._rQmax = 10000; // Max receive queue size before compacting | |
57 | this._sQ = []; // Send queue | |
58 | ||
59 | this._mode = 'base64'; // Current WebSocket mode: 'binary', 'base64' | |
60 | this.maxBufferedAmount = 200; | |
61 | ||
62 | this._eventHandlers = { | |
63 | 'message': function () {}, | |
64 | 'open': function () {}, | |
65 | 'close': function () {}, | |
66 | 'error': function () {} | |
67 | }; | |
72f1348b JM |
68 | } |
69 | ||
2cccf753 SR |
70 | (function () { |
71 | "use strict"; | |
72 | Websock.prototype = { | |
73 | // Getters and Setters | |
74 | get_sQ: function () { | |
75 | return this._sQ; | |
76 | }, | |
77 | ||
78 | get_rQ: function () { | |
79 | return this._rQ; | |
80 | }, | |
81 | ||
82 | get_rQi: function () { | |
83 | return this._rQi; | |
84 | }, | |
85 | ||
86 | set_rQi: function (val) { | |
87 | this._rQi = val; | |
88 | }, | |
89 | ||
90 | // Receive Queue | |
91 | rQlen: function () { | |
92 | return this._rQ.length - this._rQi; | |
93 | }, | |
94 | ||
95 | rQpeek8: function () { | |
96 | return this._rQ[this._rQi]; | |
97 | }, | |
98 | ||
99 | rQshift8: function () { | |
100 | return this._rQ[this._rQi++]; | |
101 | }, | |
102 | ||
b1dee947 SR |
103 | rQskip8: function () { |
104 | this._rQi++; | |
105 | }, | |
106 | ||
107 | rQskipBytes: function (num) { | |
108 | this._rQi += num; | |
109 | }, | |
110 | ||
2cccf753 SR |
111 | rQunshift8: function (num) { |
112 | if (this._rQi === 0) { | |
113 | this._rQ.unshift(num); | |
114 | } else { | |
115 | this._rQi--; | |
116 | this._rQ[this._rQi] = num; | |
117 | } | |
118 | }, | |
119 | ||
120 | rQshift16: function () { | |
121 | return (this._rQ[this._rQi++] << 8) + | |
122 | this._rQ[this._rQi++]; | |
123 | }, | |
124 | ||
125 | rQshift32: function () { | |
126 | return (this._rQ[this._rQi++] << 24) + | |
127 | (this._rQ[this._rQi++] << 16) + | |
128 | (this._rQ[this._rQi++] << 8) + | |
129 | this._rQ[this._rQi++]; | |
130 | }, | |
131 | ||
132 | rQshiftStr: function (len) { | |
133 | if (typeof(len) === 'undefined') { len = this.rQlen(); } | |
134 | var arr = this._rQ.slice(this._rQi, this._rQi + len); | |
135 | this._rQi += len; | |
136 | return String.fromCharCode.apply(null, arr); | |
137 | }, | |
138 | ||
139 | rQshiftBytes: function (len) { | |
140 | if (typeof(len) === 'undefined') { len = this.rQlen(); } | |
141 | this._rQi += len; | |
142 | return this._rQ.slice(this._rQi - len, this._rQi); | |
143 | }, | |
144 | ||
145 | rQslice: function (start, end) { | |
146 | if (end) { | |
147 | return this._rQ.slice(this._rQi + start, this._rQi + end); | |
148 | } else { | |
149 | return this._rQ.slice(this._rQi + start); | |
150 | } | |
151 | }, | |
152 | ||
153 | // Check to see if we must wait for 'num' bytes (default to FBU.bytes) | |
154 | // to be available in the receive queue. Return true if we need to | |
155 | // wait (and possibly print a debug message), otherwise false. | |
156 | rQwait: function (msg, num, goback) { | |
157 | var rQlen = this._rQ.length - this._rQi; // Skip rQlen() function call | |
158 | if (rQlen < num) { | |
159 | if (goback) { | |
160 | if (this._rQi < goback) { | |
161 | throw new Error("rQwait cannot backup " + goback + " bytes"); | |
162 | } | |
163 | this._rQi -= goback; | |
164 | } | |
165 | return true; // true means need more data | |
166 | } | |
167 | return false; | |
168 | }, | |
72f1348b | 169 | |
2cccf753 | 170 | // Send Queue |
72f1348b | 171 | |
2cccf753 SR |
172 | flush: function () { |
173 | if (this._websocket.bufferedAmount !== 0) { | |
174 | Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount); | |
175 | } | |
72f1348b | 176 | |
2cccf753 SR |
177 | if (this._websocket.bufferedAmount < this.maxBufferedAmount) { |
178 | if (this._sQ.length > 0) { | |
179 | this._websocket.send(this._encode_message()); | |
180 | this._sQ = []; | |
181 | } | |
fc003a13 | 182 | |
2cccf753 SR |
183 | return true; |
184 | } else { | |
185 | Util.Info("Delaying send, bufferedAmount: " + | |
186 | this._websocket.bufferedAmount); | |
187 | return false; | |
188 | } | |
189 | }, | |
190 | ||
191 | send: function (arr) { | |
192 | this._sQ = this._sQ.concat(arr); | |
193 | return this.flush(); | |
194 | }, | |
195 | ||
196 | send_string: function (str) { | |
197 | this.send(str.split('').map(function (chr) { | |
198 | return chr.charCodeAt(0); | |
199 | })); | |
200 | }, | |
201 | ||
202 | // Event Handlers | |
155d78b3 JS |
203 | off: function (evt) { |
204 | this._eventHandlers[evt] = function () {}; | |
205 | }, | |
206 | ||
2cccf753 SR |
207 | on: function (evt, handler) { |
208 | this._eventHandlers[evt] = handler; | |
209 | }, | |
210 | ||
211 | init: function (protocols, ws_schema) { | |
212 | this._rQ = []; | |
213 | this._rQi = 0; | |
214 | this._sQ = []; | |
215 | this._websocket = null; | |
216 | ||
217 | // Check for full typed array support | |
218 | var bt = false; | |
219 | if (('Uint8Array' in window) && | |
220 | ('set' in Uint8Array.prototype)) { | |
221 | bt = true; | |
222 | } | |
72f1348b | 223 | |
2cccf753 SR |
224 | // Check for full binary type support in WebSockets |
225 | // Inspired by: | |
226 | // https://github.com/Modernizr/Modernizr/issues/370 | |
227 | // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js | |
228 | var wsbt = false; | |
229 | try { | |
230 | if (bt && ('binaryType' in WebSocket.prototype || | |
231 | !!(new WebSocket(ws_schema + '://.').binaryType))) { | |
232 | Util.Info("Detected binaryType support in WebSockets"); | |
233 | wsbt = true; | |
234 | } | |
235 | } catch (exc) { | |
236 | // Just ignore failed test localhost connection | |
72f1348b | 237 | } |
72f1348b | 238 | |
2cccf753 SR |
239 | // Default protocols if not specified |
240 | if (typeof(protocols) === "undefined") { | |
241 | if (wsbt) { | |
242 | protocols = ['binary', 'base64']; | |
243 | } else { | |
244 | protocols = 'base64'; | |
245 | } | |
246 | } | |
72f1348b | 247 | |
2cccf753 SR |
248 | if (!wsbt) { |
249 | if (protocols === 'binary') { | |
250 | throw new Error('WebSocket binary sub-protocol requested but not supported'); | |
251 | } | |
72f1348b | 252 | |
2cccf753 SR |
253 | if (typeof(protocols) === 'object') { |
254 | var new_protocols = []; | |
255 | ||
256 | for (var i = 0; i < protocols.length; i++) { | |
257 | if (protocols[i] === 'binary') { | |
258 | Util.Error('Skipping unsupported WebSocket binary sub-protocol'); | |
259 | } else { | |
260 | new_protocols.push(protocols[i]); | |
261 | } | |
262 | } | |
263 | ||
264 | if (new_protocols.length > 0) { | |
265 | protocols = new_protocols; | |
266 | } else { | |
267 | throw new Error("Only WebSocket binary sub-protocol was requested and is not supported."); | |
268 | } | |
269 | } | |
270 | } | |
72f1348b | 271 | |
2cccf753 SR |
272 | return protocols; |
273 | }, | |
72f1348b | 274 | |
2cccf753 SR |
275 | open: function (uri, protocols) { |
276 | var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; | |
277 | protocols = this.init(protocols, ws_schema); | |
72f1348b | 278 | |
2cccf753 | 279 | this._websocket = new WebSocket(uri, protocols); |
72f1348b | 280 | |
2cccf753 SR |
281 | if (protocols.indexOf('binary') >= 0) { |
282 | this._websocket.binaryType = 'arraybuffer'; | |
72f1348b | 283 | } |
72f1348b | 284 | |
2cccf753 SR |
285 | this._websocket.onmessage = this._recv_message.bind(this); |
286 | this._websocket.onopen = (function () { | |
287 | Util.Debug('>> WebSock.onopen'); | |
288 | if (this._websocket.protocol) { | |
289 | this._mode = this._websocket.protocol; | |
290 | Util.Info("Server choose sub-protocol: " + this._websocket.protocol); | |
204675c8 | 291 | } else { |
2cccf753 SR |
292 | this._mode = 'base64'; |
293 | Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol); | |
fcff386b | 294 | } |
2cccf753 SR |
295 | this._eventHandlers.open(); |
296 | Util.Debug("<< WebSock.onopen"); | |
297 | }).bind(this); | |
298 | this._websocket.onclose = (function (e) { | |
299 | Util.Debug(">> WebSock.onclose"); | |
300 | this._eventHandlers.close(e); | |
301 | Util.Debug("<< WebSock.onclose"); | |
302 | }).bind(this); | |
303 | this._websocket.onerror = (function (e) { | |
304 | Util.Debug(">> WebSock.onerror: " + e); | |
305 | this._eventHandlers.error(e); | |
306 | Util.Debug("<< WebSock.onerror: " + e); | |
307 | }).bind(this); | |
308 | }, | |
309 | ||
310 | close: function () { | |
311 | if (this._websocket) { | |
312 | if ((this._websocket.readyState === WebSocket.OPEN) || | |
313 | (this._websocket.readyState === WebSocket.CONNECTING)) { | |
314 | Util.Info("Closing WebSocket connection"); | |
315 | this._websocket.close(); | |
316 | } | |
317 | ||
318 | this._websocket.onmessage = function (e) { return; }; | |
fcff386b | 319 | } |
2cccf753 SR |
320 | }, |
321 | ||
322 | // private methods | |
323 | _encode_message: function () { | |
324 | if (this._mode === 'binary') { | |
325 | // Put in a binary arraybuffer | |
326 | return (new Uint8Array(this._sQ)).buffer; | |
204675c8 | 327 | } else { |
2cccf753 SR |
328 | // base64 encode |
329 | return Base64.encode(this._sQ); | |
204675c8 | 330 | } |
2cccf753 SR |
331 | }, |
332 | ||
333 | _decode_message: function (data) { | |
334 | if (this._mode === 'binary') { | |
335 | // push arraybuffer values onto the end | |
336 | var u8 = new Uint8Array(data); | |
337 | for (var i = 0; i < u8.length; i++) { | |
338 | this._rQ.push(u8[i]); | |
339 | } | |
340 | } else { | |
341 | // base64 decode and concat to end | |
342 | this._rQ = this._rQ.concat(Base64.decode(data, 0)); | |
343 | } | |
344 | }, | |
345 | ||
346 | _recv_message: function (e) { | |
347 | try { | |
348 | this._decode_message(e.data); | |
349 | if (this.rQlen() > 0) { | |
350 | this._eventHandlers.message(); | |
351 | // Compact the receive queue | |
352 | if (this._rQ.length > this._rQmax) { | |
353 | this._rQ = this._rQ.slice(this._rQi); | |
354 | this._rQi = 0; | |
355 | } | |
356 | } else { | |
357 | Util.Debug("Ignoring empty message"); | |
358 | } | |
359 | } catch (exc) { | |
360 | var exception_str = ""; | |
361 | if (exc.name) { | |
362 | exception_str += "\n name: " + exc.name + "\n"; | |
363 | exception_str += " message: " + exc.message + "\n"; | |
364 | } | |
fcff386b | 365 | |
2cccf753 SR |
366 | if (typeof exc.description !== 'undefined') { |
367 | exception_str += " description: " + exc.description + "\n"; | |
368 | } | |
72f1348b | 369 | |
2cccf753 SR |
370 | if (typeof exc.stack !== 'undefined') { |
371 | exception_str += exc.stack; | |
372 | } | |
72f1348b | 373 | |
2cccf753 SR |
374 | if (exception_str.length > 0) { |
375 | Util.Error("recv_message, caught exception: " + exception_str); | |
376 | } else { | |
377 | Util.Error("recv_message, caught exception: " + exc); | |
378 | } | |
72f1348b | 379 | |
2cccf753 SR |
380 | if (typeof exc.name !== 'undefined') { |
381 | this._eventHandlers.error(exc.name + ": " + exc.message); | |
382 | } else { | |
383 | this._eventHandlers.error(exc); | |
384 | } | |
385 | } | |
72f1348b | 386 | } |
2cccf753 SR |
387 | }; |
388 | })(); |