Home | History | Annotate | Download | only in include
      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 
     28 if (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 
     50 function 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