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