]>
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 */ |
38781d93 | 18 | /*global Util*/ |
ff4bfcb7 | 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 | 36 | Websock_native = false; |
72f1348b JM |
37 | } |
38 | ||
72f1348b | 39 | function Websock() { |
2cccf753 SR |
40 | "use strict"; |
41 | ||
42 | this._websocket = null; // WebSocket object | |
38781d93 | 43 | |
2cccf753 | 44 | this._rQi = 0; // Receive queue index |
38781d93 SR |
45 | this._rQlen = 0; // Next write position in the receive queue |
46 | this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB) | |
47 | this._rQmax = this._rQbufferSize / 8; | |
2cccf753 | 48 | this._sQ = []; // Send queue |
38781d93 SR |
49 | // called in init: this._rQ = new Uint8Array(this._rQbufferSize); |
50 | this._rQ = null; // Receive queue | |
2cccf753 | 51 | |
38781d93 | 52 | this._mode = 'binary'; // Current WebSocket mode: 'binary', 'base64' |
2cccf753 SR |
53 | this.maxBufferedAmount = 200; |
54 | ||
55 | this._eventHandlers = { | |
56 | 'message': function () {}, | |
57 | 'open': function () {}, | |
58 | 'close': function () {}, | |
59 | 'error': function () {} | |
60 | }; | |
72f1348b JM |
61 | } |
62 | ||
2cccf753 SR |
63 | (function () { |
64 | "use strict"; | |
38781d93 SR |
65 | |
66 | var typedArrayToString = (function () { | |
67 | // This is only for PhantomJS, which doesn't like apply-ing | |
68 | // with Typed Arrays | |
69 | try { | |
70 | var arr = new Uint8Array([1, 2, 3]); | |
71 | String.fromCharCode.apply(null, arr); | |
72 | return function (a) { return String.fromCharCode.apply(null, a); }; | |
73 | } catch (ex) { | |
74 | return function (a) { | |
75 | return String.fromCharCode.apply( | |
76 | null, Array.prototype.slice.call(a)); | |
77 | }; | |
78 | } | |
79 | })(); | |
80 | ||
2cccf753 SR |
81 | Websock.prototype = { |
82 | // Getters and Setters | |
83 | get_sQ: function () { | |
84 | return this._sQ; | |
85 | }, | |
86 | ||
87 | get_rQ: function () { | |
88 | return this._rQ; | |
89 | }, | |
90 | ||
91 | get_rQi: function () { | |
92 | return this._rQi; | |
93 | }, | |
94 | ||
95 | set_rQi: function (val) { | |
96 | this._rQi = val; | |
97 | }, | |
98 | ||
99 | // Receive Queue | |
100 | rQlen: function () { | |
38781d93 | 101 | return this._rQlen - this._rQi; |
2cccf753 SR |
102 | }, |
103 | ||
104 | rQpeek8: function () { | |
105 | return this._rQ[this._rQi]; | |
106 | }, | |
107 | ||
108 | rQshift8: function () { | |
109 | return this._rQ[this._rQi++]; | |
110 | }, | |
111 | ||
b1dee947 SR |
112 | rQskip8: function () { |
113 | this._rQi++; | |
114 | }, | |
115 | ||
116 | rQskipBytes: function (num) { | |
117 | this._rQi += num; | |
118 | }, | |
119 | ||
38781d93 | 120 | // TODO(directxman12): test performance with these vs a DataView |
2cccf753 SR |
121 | rQshift16: function () { |
122 | return (this._rQ[this._rQi++] << 8) + | |
123 | this._rQ[this._rQi++]; | |
124 | }, | |
125 | ||
126 | rQshift32: function () { | |
127 | return (this._rQ[this._rQi++] << 24) + | |
128 | (this._rQ[this._rQi++] << 16) + | |
129 | (this._rQ[this._rQi++] << 8) + | |
130 | this._rQ[this._rQi++]; | |
131 | }, | |
132 | ||
133 | rQshiftStr: function (len) { | |
134 | if (typeof(len) === 'undefined') { len = this.rQlen(); } | |
38781d93 | 135 | var arr = new Uint8Array(this._rQ.buffer, this._rQi, len); |
2cccf753 | 136 | this._rQi += len; |
38781d93 | 137 | return typedArrayToString(arr); |
2cccf753 SR |
138 | }, |
139 | ||
140 | rQshiftBytes: function (len) { | |
141 | if (typeof(len) === 'undefined') { len = this.rQlen(); } | |
142 | this._rQi += len; | |
38781d93 SR |
143 | return new Uint8Array(this._rQ.buffer, this._rQi - len, len); |
144 | }, | |
145 | ||
146 | rQshiftTo: function (target, len) { | |
147 | if (len === undefined) { len = this.rQlen(); } | |
148 | // TODO: make this just use set with views when using a ArrayBuffer to store the rQ | |
149 | target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); | |
150 | this._rQi += len; | |
2cccf753 SR |
151 | }, |
152 | ||
153 | rQslice: function (start, end) { | |
154 | if (end) { | |
38781d93 | 155 | return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); |
2cccf753 | 156 | } else { |
38781d93 | 157 | return new Uint8Array(this._rQ.buffer, this._rQi + start, this._rQlen - this._rQi - start); |
2cccf753 SR |
158 | } |
159 | }, | |
160 | ||
161 | // Check to see if we must wait for 'num' bytes (default to FBU.bytes) | |
162 | // to be available in the receive queue. Return true if we need to | |
163 | // wait (and possibly print a debug message), otherwise false. | |
164 | rQwait: function (msg, num, goback) { | |
38781d93 | 165 | var rQlen = this._rQlen - this._rQi; // Skip rQlen() function call |
2cccf753 SR |
166 | if (rQlen < num) { |
167 | if (goback) { | |
168 | if (this._rQi < goback) { | |
169 | throw new Error("rQwait cannot backup " + goback + " bytes"); | |
170 | } | |
171 | this._rQi -= goback; | |
172 | } | |
173 | return true; // true means need more data | |
174 | } | |
175 | return false; | |
176 | }, | |
72f1348b | 177 | |
2cccf753 | 178 | // Send Queue |
72f1348b | 179 | |
2cccf753 SR |
180 | flush: function () { |
181 | if (this._websocket.bufferedAmount !== 0) { | |
182 | Util.Debug("bufferedAmount: " + this._websocket.bufferedAmount); | |
183 | } | |
72f1348b | 184 | |
2cccf753 SR |
185 | if (this._websocket.bufferedAmount < this.maxBufferedAmount) { |
186 | if (this._sQ.length > 0) { | |
187 | this._websocket.send(this._encode_message()); | |
188 | this._sQ = []; | |
189 | } | |
fc003a13 | 190 | |
2cccf753 SR |
191 | return true; |
192 | } else { | |
193 | Util.Info("Delaying send, bufferedAmount: " + | |
194 | this._websocket.bufferedAmount); | |
195 | return false; | |
196 | } | |
197 | }, | |
198 | ||
199 | send: function (arr) { | |
200 | this._sQ = this._sQ.concat(arr); | |
201 | return this.flush(); | |
202 | }, | |
203 | ||
204 | send_string: function (str) { | |
205 | this.send(str.split('').map(function (chr) { | |
206 | return chr.charCodeAt(0); | |
207 | })); | |
208 | }, | |
209 | ||
210 | // Event Handlers | |
155d78b3 JS |
211 | off: function (evt) { |
212 | this._eventHandlers[evt] = function () {}; | |
213 | }, | |
214 | ||
2cccf753 SR |
215 | on: function (evt, handler) { |
216 | this._eventHandlers[evt] = handler; | |
217 | }, | |
218 | ||
38781d93 SR |
219 | _allocate_buffers: function () { |
220 | this._rQ = new Uint8Array(this._rQbufferSize); | |
221 | }, | |
222 | ||
2cccf753 | 223 | init: function (protocols, ws_schema) { |
38781d93 | 224 | this._allocate_buffers(); |
2cccf753 SR |
225 | this._rQi = 0; |
226 | this._sQ = []; | |
227 | this._websocket = null; | |
228 | ||
229 | // Check for full typed array support | |
230 | var bt = false; | |
231 | if (('Uint8Array' in window) && | |
232 | ('set' in Uint8Array.prototype)) { | |
233 | bt = true; | |
234 | } | |
72f1348b | 235 | |
2cccf753 SR |
236 | // Check for full binary type support in WebSockets |
237 | // Inspired by: | |
238 | // https://github.com/Modernizr/Modernizr/issues/370 | |
239 | // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/websockets/binary.js | |
240 | var wsbt = false; | |
241 | try { | |
242 | if (bt && ('binaryType' in WebSocket.prototype || | |
243 | !!(new WebSocket(ws_schema + '://.').binaryType))) { | |
244 | Util.Info("Detected binaryType support in WebSockets"); | |
245 | wsbt = true; | |
246 | } | |
247 | } catch (exc) { | |
248 | // Just ignore failed test localhost connection | |
72f1348b | 249 | } |
72f1348b | 250 | |
2cccf753 SR |
251 | // Default protocols if not specified |
252 | if (typeof(protocols) === "undefined") { | |
38781d93 | 253 | protocols = 'binary'; |
2cccf753 | 254 | } |
72f1348b | 255 | |
38781d93 SR |
256 | if (Array.isArray(protocols) && protocols.indexOf('binary') > -1) { |
257 | protocols = 'binary'; | |
258 | } | |
2cccf753 | 259 | |
38781d93 SR |
260 | if (!wsbt) { |
261 | throw new Error("noVNC no longer supports base64 WebSockets. " + | |
262 | "Please use a browser which supports binary WebSockets."); | |
263 | } | |
2cccf753 | 264 | |
38781d93 SR |
265 | if (protocols != 'binary') { |
266 | throw new Error("noVNC no longer supports base64 WebSockets. Please " + | |
267 | "use the binary subprotocol instead."); | |
2cccf753 | 268 | } |
72f1348b | 269 | |
2cccf753 SR |
270 | return protocols; |
271 | }, | |
72f1348b | 272 | |
2cccf753 SR |
273 | open: function (uri, protocols) { |
274 | var ws_schema = uri.match(/^([a-z]+):\/\//)[1]; | |
275 | protocols = this.init(protocols, ws_schema); | |
72f1348b | 276 | |
2cccf753 | 277 | this._websocket = new WebSocket(uri, protocols); |
72f1348b | 278 | |
2cccf753 SR |
279 | if (protocols.indexOf('binary') >= 0) { |
280 | this._websocket.binaryType = 'arraybuffer'; | |
72f1348b | 281 | } |
72f1348b | 282 | |
2cccf753 SR |
283 | this._websocket.onmessage = this._recv_message.bind(this); |
284 | this._websocket.onopen = (function () { | |
285 | Util.Debug('>> WebSock.onopen'); | |
286 | if (this._websocket.protocol) { | |
287 | this._mode = this._websocket.protocol; | |
288 | Util.Info("Server choose sub-protocol: " + this._websocket.protocol); | |
204675c8 | 289 | } else { |
38781d93 | 290 | this._mode = 'binary'; |
2cccf753 | 291 | Util.Error('Server select no sub-protocol!: ' + this._websocket.protocol); |
fcff386b | 292 | } |
38781d93 SR |
293 | |
294 | if (this._mode != 'binary') { | |
295 | throw new Error("noVNC no longer supports base64 WebSockets. Please " + | |
296 | "use the binary subprotocol instead."); | |
297 | ||
298 | } | |
299 | ||
2cccf753 SR |
300 | this._eventHandlers.open(); |
301 | Util.Debug("<< WebSock.onopen"); | |
302 | }).bind(this); | |
303 | this._websocket.onclose = (function (e) { | |
304 | Util.Debug(">> WebSock.onclose"); | |
305 | this._eventHandlers.close(e); | |
306 | Util.Debug("<< WebSock.onclose"); | |
307 | }).bind(this); | |
308 | this._websocket.onerror = (function (e) { | |
309 | Util.Debug(">> WebSock.onerror: " + e); | |
310 | this._eventHandlers.error(e); | |
311 | Util.Debug("<< WebSock.onerror: " + e); | |
312 | }).bind(this); | |
313 | }, | |
314 | ||
315 | close: function () { | |
316 | if (this._websocket) { | |
317 | if ((this._websocket.readyState === WebSocket.OPEN) || | |
318 | (this._websocket.readyState === WebSocket.CONNECTING)) { | |
319 | Util.Info("Closing WebSocket connection"); | |
320 | this._websocket.close(); | |
321 | } | |
322 | ||
323 | this._websocket.onmessage = function (e) { return; }; | |
fcff386b | 324 | } |
2cccf753 SR |
325 | }, |
326 | ||
327 | // private methods | |
328 | _encode_message: function () { | |
38781d93 SR |
329 | // Put in a binary arraybuffer |
330 | return (new Uint8Array(this._sQ)).buffer; | |
2cccf753 SR |
331 | }, |
332 | ||
333 | _decode_message: function (data) { | |
38781d93 SR |
334 | // push arraybuffer values onto the end |
335 | var u8 = new Uint8Array(data); | |
336 | this._rQ.set(u8, this._rQlen); | |
337 | this._rQlen += u8.length; | |
2cccf753 SR |
338 | }, |
339 | ||
340 | _recv_message: function (e) { | |
341 | try { | |
342 | this._decode_message(e.data); | |
343 | if (this.rQlen() > 0) { | |
344 | this._eventHandlers.message(); | |
345 | // Compact the receive queue | |
38781d93 SR |
346 | if (this._rQlen == this._rQi) { |
347 | this._rQlen = 0; | |
348 | this._rQi = 0; | |
349 | } else if (this._rQlen > this._rQmax) { | |
350 | if (this._rQlen - this._rQi > 0.5 * this._rQbufferSize) { | |
351 | var old_rQbuffer = this._rQ.buffer; | |
352 | this._rQbufferSize *= 2; | |
353 | this._rQmax = this._rQbufferSize / 8; | |
354 | this._rQ = new Uint8Array(this._rQbufferSize); | |
355 | this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); | |
356 | } else { | |
357 | if (this._rQ.copyWithin) { | |
358 | // Firefox only, ATM | |
359 | this._rQ.copyWithin(0, this._rQi); | |
360 | } else { | |
361 | this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); | |
362 | } | |
363 | } | |
364 | ||
365 | this._rQlen = this._rQlen - this._rQi; | |
2cccf753 SR |
366 | this._rQi = 0; |
367 | } | |
368 | } else { | |
369 | Util.Debug("Ignoring empty message"); | |
370 | } | |
371 | } catch (exc) { | |
372 | var exception_str = ""; | |
373 | if (exc.name) { | |
374 | exception_str += "\n name: " + exc.name + "\n"; | |
375 | exception_str += " message: " + exc.message + "\n"; | |
376 | } | |
fcff386b | 377 | |
2cccf753 SR |
378 | if (typeof exc.description !== 'undefined') { |
379 | exception_str += " description: " + exc.description + "\n"; | |
380 | } | |
72f1348b | 381 | |
2cccf753 SR |
382 | if (typeof exc.stack !== 'undefined') { |
383 | exception_str += exc.stack; | |
384 | } | |
72f1348b | 385 | |
2cccf753 SR |
386 | if (exception_str.length > 0) { |
387 | Util.Error("recv_message, caught exception: " + exception_str); | |
388 | } else { | |
389 | Util.Error("recv_message, caught exception: " + exc); | |
390 | } | |
72f1348b | 391 | |
2cccf753 SR |
392 | if (typeof exc.name !== 'undefined') { |
393 | this._eventHandlers.error(exc.name + ": " + exc.message); | |
394 | } else { | |
395 | this._eventHandlers.error(exc); | |
396 | } | |
397 | } | |
72f1348b | 398 | } |
2cccf753 SR |
399 | }; |
400 | })(); |