1/*
2 * Websock: high-performance binary WebSockets
3 * Copyright (C) 2012 Joel Martin
4 * Licensed under MPL 2.0 (see LICENSE.txt)
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
17/*jslint browser: true, bitwise: true */
18/*global Util, Base64 */
19
20
21// Load Flash WebSocket emulator if needed
22
23// To force WebSocket emulator even when native WebSocket available
24//window.WEB_SOCKET_FORCE_FLASH = true;
25// To enable WebSocket emulator debug:
26//window.WEB_SOCKET_DEBUG=1;
27
28if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) {
29    Websock_native = true;
30} else if (window.MozWebSocket && !window.WEB_SOCKET_FORCE_FLASH) {
31    Websock_native = true;
32    window.WebSocket = window.MozWebSocket;
33} else {
34    /* no builtin WebSocket so load web_socket.js */
35
36    Websock_native = false;
37    (function () {
38        window.WEB_SOCKET_SWF_LOCATION = Util.get_include_uri() +
39                    "web-socket-js/WebSocketMain.swf";
40        if (Util.Engine.trident) {
41            Util.Debug("Forcing uncached load of WebSocketMain.swf");
42            window.WEB_SOCKET_SWF_LOCATION += "?" + Math.random();
43        }
44        Util.load_scripts(["web-socket-js/swfobject.js",
45                           "web-socket-js/web_socket.js"]);
46    })();
47}
48
49
50function Websock() {
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    };
68}
69
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
103        rQskip8: function () {
104            this._rQi++;
105        },
106
107        rQskipBytes: function (num) {
108            this._rQi += num;
109        },
110
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        },
169
170        // Send Queue
171
172        flush: function () {
173            if (this._websocket.bufferedAmount !== 0) {
174                Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount);
175            }
176
177            if (this._websocket.bufferedAmount < this.maxBufferedAmount) {
178                if (this._sQ.length > 0) {
179                    this._websocket.send(this._encode_message());
180                    this._sQ = [];
181                }
182
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
203        on: function (evt, handler) {
204            this._eventHandlers[evt] = handler;
205        },
206
207        init: function (protocols, ws_schema) {
208            this._rQ = [];
209            this._rQi = 0;
210            this._sQ = [];
211            this._websocket = null;
212
213            // Check for full typed array support
214            var bt = false;
215            if (('Uint8Array' in window) &&
216                    ('set' in Uint8Array.prototype)) {
217                bt = true;
218            }
219
220            // Check for full binary type support in WebSockets
221            // Inspired by:
222            // https://github.com/Modernizr/Modernizr/issues/370
223            // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js
224            var wsbt = false;
225            try {
226                if (bt && ('binaryType' in WebSocket.prototype ||
227                           !!(new WebSocket(ws_schema + '://.').binaryType))) {
228                    Util.Info("Detected binaryType support in WebSockets");
229                    wsbt = true;
230                }
231            } catch (exc) {
232                // Just ignore failed test localhost connection
233            }
234
235            // Default protocols if not specified
236            if (typeof(protocols) === "undefined") {
237                if (wsbt) {
238                    protocols = ['binary', 'base64'];
239                } else {
240                    protocols = 'base64';
241                }
242            }
243
244            if (!wsbt) {
245                if (protocols === 'binary') {
246                    throw new Error('WebSocket binary sub-protocol requested but not supported');
247                }
248
249                if (typeof(protocols) === 'object') {
250                    var new_protocols = [];
251
252                    for (var i = 0; i < protocols.length; i++) {
253                        if (protocols[i] === 'binary') {
254                            Util.Error('Skipping unsupported WebSocket binary sub-protocol');
255                        } else {
256                            new_protocols.push(protocols[i]);
257                        }
258                    }
259
260                    if (new_protocols.length > 0) {
261                        protocols = new_protocols;
262                    } else {
263                        throw new Error("Only WebSocket binary sub-protocol was requested and is not supported.");
264                    }
265                }
266            }
267
268            return protocols;
269        },
270
271        open: function (uri, protocols) {
272            var ws_schema = uri.match(/^([a-z]+):\/\//)[1];
273            protocols = this.init(protocols, ws_schema);
274
275            this._websocket = new WebSocket(uri, protocols);
276
277            if (protocols.indexOf('binary') >= 0) {
278                this._websocket.binaryType = 'arraybuffer';
279            }
280
281            this._websocket.onmessage = this._recv_message.bind(this);
282            this._websocket.onopen = (function () {
283                Util.Debug('>> WebSock.onopen');
284                if (this._websocket.protocol) {
285                    this._mode = this._websocket.protocol;
286                    Util.Info("Server choose sub-protocol: " + this._websocket.protocol);
287                } else {
288                    this._mode = 'base64';
289                    Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol);
290                }
291                this._eventHandlers.open();
292                Util.Debug("<< WebSock.onopen");
293            }).bind(this);
294            this._websocket.onclose = (function (e) {
295                Util.Debug(">> WebSock.onclose");
296                this._eventHandlers.close(e);
297                Util.Debug("<< WebSock.onclose");
298            }).bind(this);
299            this._websocket.onerror = (function (e) {
300                Util.Debug(">> WebSock.onerror: " + e);
301                this._eventHandlers.error(e);
302                Util.Debug("<< WebSock.onerror: " + e);
303            }).bind(this);
304        },
305
306        close: function () {
307            if (this._websocket) {
308                if ((this._websocket.readyState === WebSocket.OPEN) ||
309                        (this._websocket.readyState === WebSocket.CONNECTING)) {
310                    Util.Info("Closing WebSocket connection");
311                    this._websocket.close();
312                }
313
314                this._websocket.onmessage = function (e) { return; };
315            }
316        },
317
318        // private methods
319        _encode_message: function () {
320            if (this._mode === 'binary') {
321                // Put in a binary arraybuffer
322                return (new Uint8Array(this._sQ)).buffer;
323            } else {
324                // base64 encode
325                return Base64.encode(this._sQ);
326            }
327        },
328
329        _decode_message: function (data) {
330            if (this._mode === 'binary') {
331                // push arraybuffer values onto the end
332                var u8 = new Uint8Array(data);
333                for (var i = 0; i < u8.length; i++) {
334                    this._rQ.push(u8[i]);
335                }
336            } else {
337                // base64 decode and concat to end
338                this._rQ = this._rQ.concat(Base64.decode(data, 0));
339            }
340        },
341
342        _recv_message: function (e) {
343            try {
344                this._decode_message(e.data);
345                if (this.rQlen() > 0) {
346                    this._eventHandlers.message();
347                    // Compact the receive queue
348                    if (this._rQ.length > this._rQmax) {
349                        this._rQ = this._rQ.slice(this._rQi);
350                        this._rQi = 0;
351                    }
352                } else {
353                    Util.Debug("Ignoring empty message");
354                }
355            } catch (exc) {
356                var exception_str = "";
357                if (exc.name) {
358                    exception_str += "\n    name: " + exc.name + "\n";
359                    exception_str += "    message: " + exc.message + "\n";
360                }
361
362                if (typeof exc.description !== 'undefined') {
363                    exception_str += "    description: " + exc.description + "\n";
364                }
365
366                if (typeof exc.stack !== 'undefined') {
367                    exception_str += exc.stack;
368                }
369
370                if (exception_str.length > 0) {
371                    Util.Error("recv_message, caught exception: " + exception_str);
372                } else {
373                    Util.Error("recv_message, caught exception: " + exc);
374                }
375
376                if (typeof exc.name !== 'undefined') {
377                    this._eventHandlers.error(exc.name + ": " + exc.message);
378                } else {
379                    this._eventHandlers.error(exc);
380                }
381            }
382        }
383    };
384})();
385