/* * Websock: high-performance binary WebSockets * Copyright (C) 2012 Joel Martin * Licensed under MPL 2.0 (see LICENSE.txt) * * Websock is similar to the standard WebSocket object but Websock * enables communication with raw TCP sockets (i.e. the binary stream) * via websockify. This is accomplished by base64 encoding the data * stream between Websock and websockify. * * Websock has built-in receive queue buffering; the message event * does not contain actual data but is simply a notification that * there is new data available. Several rQ* methods are available to * read binary data off of the receive queue. */ /*jslint browser: true, bitwise: true */ /*global Util, Base64 */ // Load Flash WebSocket emulator if needed // To force WebSocket emulator even when native WebSocket available //window.WEB_SOCKET_FORCE_FLASH = true; // To enable WebSocket emulator debug: //window.WEB_SOCKET_DEBUG=1; if (window.WebSocket && !window.WEB_SOCKET_FORCE_FLASH) { Websock_native = true; } else if (window.MozWebSocket && !window.WEB_SOCKET_FORCE_FLASH) { Websock_native = true; window.WebSocket = window.MozWebSocket; } else { /* no builtin WebSocket so load web_socket.js */ Websock_native = false; (function () { window.WEB_SOCKET_SWF_LOCATION = Util.get_include_uri() + "web-socket-js/WebSocketMain.swf"; if (Util.Engine.trident) { Util.Debug("Forcing uncached load of WebSocketMain.swf"); window.WEB_SOCKET_SWF_LOCATION += "?" + Math.random(); } Util.load_scripts(["web-socket-js/swfobject.js", "web-socket-js/web_socket.js"]); })(); } function Websock() { "use strict"; this._websocket = null; // WebSocket object this._rQ = []; // Receive queue this._rQi = 0; // Receive queue index this._rQmax = 10000; // Max receive queue size before compacting this._sQ = []; // Send queue this._mode = 'base64'; // Current WebSocket mode: 'binary', 'base64' this.maxBufferedAmount = 200; this._eventHandlers = { 'message': function () {}, 'open': function () {}, 'close': function () {}, 'error': function () {} }; } (function () { "use strict"; Websock.prototype = { // Getters and Setters get_sQ: function () { return this._sQ; }, get_rQ: function () { return this._rQ; }, get_rQi: function () { return this._rQi; }, set_rQi: function (val) { this._rQi = val; }, // Receive Queue rQlen: function () { return this._rQ.length - this._rQi; }, rQpeek8: function () { return this._rQ[this._rQi]; }, rQshift8: function () { return this._rQ[this._rQi++]; }, rQskip8: function () { this._rQi++; }, rQskipBytes: function (num) { this._rQi += num; }, rQunshift8: function (num) { if (this._rQi === 0) { this._rQ.unshift(num); } else { this._rQi--; this._rQ[this._rQi] = num; } }, rQshift16: function () { return (this._rQ[this._rQi++] << 8) + this._rQ[this._rQi++]; }, rQshift32: function () { return (this._rQ[this._rQi++] << 24) + (this._rQ[this._rQi++] << 16) + (this._rQ[this._rQi++] << 8) + this._rQ[this._rQi++]; }, rQshiftStr: function (len) { if (typeof(len) === 'undefined') { len = this.rQlen(); } var arr = this._rQ.slice(this._rQi, this._rQi + len); this._rQi += len; return String.fromCharCode.apply(null, arr); }, rQshiftBytes: function (len) { if (typeof(len) === 'undefined') { len = this.rQlen(); } this._rQi += len; return this._rQ.slice(this._rQi - len, this._rQi); }, rQslice: function (start, end) { if (end) { return this._rQ.slice(this._rQi + start, this._rQi + end); } else { return this._rQ.slice(this._rQi + start); } }, // Check to see if we must wait for 'num' bytes (default to FBU.bytes) // to be available in the receive queue. Return true if we need to // wait (and possibly print a debug message), otherwise false. rQwait: function (msg, num, goback) { var rQlen = this._rQ.length - this._rQi; // Skip rQlen() function call if (rQlen < num) { if (goback) { if (this._rQi < goback) { throw new Error("rQwait cannot backup " + goback + " bytes"); } this._rQi -= goback; } return true; // true means need more data } return false; }, // Send Queue flush: function () { if (this._websocket.bufferedAmount !== 0) { Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount); } if (this._websocket.bufferedAmount < this.maxBufferedAmount) { if (this._sQ.length > 0) { this._websocket.send(this._encode_message()); this._sQ = []; } return true; } else { Util.Info("Delaying send, bufferedAmount: " + this._websocket.bufferedAmount); return false; } }, send: function (arr) { this._sQ = this._sQ.concat(arr); return this.flush(); }, send_string: function (str) { this.send(str.split('').map(function (chr) { return chr.charCodeAt(0); })); }, // Event Handlers on: function (evt, handler) { this._eventHandlers[evt] = handler; }, init: function (protocols, ws_schema) { this._rQ = []; this._rQi = 0; this._sQ = []; this._websocket = null; // Check for full typed array support var bt = false; if (('Uint8Array' in window) && ('set' in Uint8Array.prototype)) { bt = true; } // Check for full binary type support in WebSockets // Inspired by: // https://github.com/Modernizr/Modernizr/issues/370 // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js var wsbt = false; try { if (bt && ('binaryType' in WebSocket.prototype || !!(new WebSocket(ws_schema + '://.').binaryType))) { Util.Info("Detected binaryType support in WebSockets"); wsbt = true; } } catch (exc) { // Just ignore failed test localhost connection } // Default protocols if not specified if (typeof(protocols) === "undefined") { if (wsbt) { protocols = ['binary', 'base64']; } else { protocols = 'base64'; } } if (!wsbt) { if (protocols === 'binary') { throw new Error('WebSocket binary sub-protocol requested but not supported'); } if (typeof(protocols) === 'object') { var new_protocols = []; for (var i = 0; i < protocols.length; i++) { if (protocols[i] === 'binary') { Util.Error('Skipping unsupported WebSocket binary sub-protocol'); } else { new_protocols.push(protocols[i]); } } if (new_protocols.length > 0) { protocols = new_protocols; } else { throw new Error("Only WebSocket binary sub-protocol was requested and is not supported."); } } } return protocols; }, open: function (uri, protocols) { var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; protocols = this.init(protocols, ws_schema); this._websocket = new WebSocket(uri, protocols); if (protocols.indexOf('binary') >= 0) { this._websocket.binaryType = 'arraybuffer'; } this._websocket.onmessage = this._recv_message.bind(this); this._websocket.onopen = (function () { Util.Debug('>> WebSock.onopen'); if (this._websocket.protocol) { this._mode = this._websocket.protocol; Util.Info("Server choose sub-protocol: " + this._websocket.protocol); } else { this._mode = 'base64'; Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol); } this._eventHandlers.open(); Util.Debug("<< WebSock.onopen"); }).bind(this); this._websocket.onclose = (function (e) { Util.Debug(">> WebSock.onclose"); this._eventHandlers.close(e); Util.Debug("<< WebSock.onclose"); }).bind(this); this._websocket.onerror = (function (e) { Util.Debug(">> WebSock.onerror: " + e); this._eventHandlers.error(e); Util.Debug("<< WebSock.onerror: " + e); }).bind(this); }, close: function () { if (this._websocket) { if ((this._websocket.readyState === WebSocket.OPEN) || (this._websocket.readyState === WebSocket.CONNECTING)) { Util.Info("Closing WebSocket connection"); this._websocket.close(); } this._websocket.onmessage = function (e) { return; }; } }, // private methods _encode_message: function () { if (this._mode === 'binary') { // Put in a binary arraybuffer return (new Uint8Array(this._sQ)).buffer; } else { // base64 encode return Base64.encode(this._sQ); } }, _decode_message: function (data) { if (this._mode === 'binary') { // push arraybuffer values onto the end var u8 = new Uint8Array(data); for (var i = 0; i < u8.length; i++) { this._rQ.push(u8[i]); } } else { // base64 decode and concat to end this._rQ = this._rQ.concat(Base64.decode(data, 0)); } }, _recv_message: function (e) { try { this._decode_message(e.data); if (this.rQlen() > 0) { this._eventHandlers.message(); // Compact the receive queue if (this._rQ.length > this._rQmax) { this._rQ = this._rQ.slice(this._rQi); this._rQi = 0; } } else { Util.Debug("Ignoring empty message"); } } catch (exc) { var exception_str = ""; if (exc.name) { exception_str += "\n name: " + exc.name + "\n"; exception_str += " message: " + exc.message + "\n"; } if (typeof exc.description !== 'undefined') { exception_str += " description: " + exc.description + "\n"; } if (typeof exc.stack !== 'undefined') { exception_str += exc.stack; } if (exception_str.length > 0) { Util.Error("recv_message, caught exception: " + exception_str); } else { Util.Error("recv_message, caught exception: " + exc); } if (typeof exc.name !== 'undefined') { this._eventHandlers.error(exc.name + ": " + exc.message); } else { this._eventHandlers.error(exc); } } } }; })();