]> git.proxmox.com Git - mirror_novnc.git/blob - include/rfb.js
Throw exceptions from RFB constructor
[mirror_novnc.git] / include / rfb.js
1 /*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2012 Joel Martin
4 * Copyright (C) 2013 Samuel Mannehed for Cendio AB
5 * Licensed under MPL 2.0 (see LICENSE.txt)
6 *
7 * See README.md for usage and integration instructions.
8 *
9 * TIGHT decoder portion:
10 * (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
11 */
12
13 /*jslint white: false, browser: true */
14 /*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES */
15
16 var RFB;
17
18 (function () {
19 "use strict";
20 RFB = function (defaults) {
21 if (!defaults) {
22 defaults = {};
23 }
24
25 this._rfb_host = '';
26 this._rfb_port = 5900;
27 this._rfb_password = '';
28 this._rfb_path = '';
29
30 this._rfb_state = 'disconnected';
31 this._rfb_version = 0;
32 this._rfb_max_version = 3.8;
33 this._rfb_auth_scheme = '';
34
35 this._rfb_tightvnc = false;
36 this._rfb_xvp_ver = 0;
37
38 // In preference order
39 this._encodings = [
40 ['COPYRECT', 0x01 ],
41 ['TIGHT', 0x07 ],
42 ['TIGHT_PNG', -260 ],
43 ['HEXTILE', 0x05 ],
44 ['RRE', 0x02 ],
45 ['RAW', 0x00 ],
46 ['DesktopSize', -223 ],
47 ['Cursor', -239 ],
48
49 // Psuedo-encoding settings
50 //['JPEG_quality_lo', -32 ],
51 ['JPEG_quality_med', -26 ],
52 //['JPEG_quality_hi', -23 ],
53 //['compress_lo', -255 ],
54 ['compress_hi', -247 ],
55 ['last_rect', -224 ],
56 ['xvp', -309 ],
57 ['ExtendedDesktopSize', -308 ]
58 ];
59
60 this._encHandlers = {};
61 this._encNames = {};
62 this._encStats = {};
63
64 this._sock = null; // Websock object
65 this._display = null; // Display object
66 this._keyboard = null; // Keyboard input handler object
67 this._mouse = null; // Mouse input handler object
68 this._sendTimer = null; // Send Queue check timer
69 this._disconnTimer = null; // disconnection timer
70 this._msgTimer = null; // queued handle_msg timer
71
72 // Frame buffer update state
73 this._FBU = {
74 rects: 0,
75 subrects: 0, // RRE
76 lines: 0, // RAW
77 tiles: 0, // HEXTILE
78 bytes: 0,
79 x: 0,
80 y: 0,
81 width: 0,
82 height: 0,
83 encoding: 0,
84 subencoding: -1,
85 background: null,
86 zlib: [] // TIGHT zlib streams
87 };
88
89 this._fb_Bpp = 4;
90 this._fb_depth = 3;
91 this._fb_width = 0;
92 this._fb_height = 0;
93 this._fb_name = "";
94
95 this._rre_chunk_sz = 100;
96
97 this._timing = {
98 last_fbu: 0,
99 fbu_total: 0,
100 fbu_total_cnt: 0,
101 full_fbu_total: 0,
102 full_fbu_cnt: 0,
103
104 fbu_rt_start: 0,
105 fbu_rt_total: 0,
106 fbu_rt_cnt: 0,
107 pixels: 0
108 };
109
110 this._supportsSetDesktopSize = false;
111 this._screen_id = 0;
112 this._screen_flags = 0;
113
114 // Mouse state
115 this._mouse_buttonMask = 0;
116 this._mouse_arr = [];
117 this._viewportDragging = false;
118 this._viewportDragPos = {};
119
120 // set the default value on user-facing properties
121 Util.set_defaults(this, defaults, {
122 'target': 'null', // VNC display rendering Canvas object
123 'focusContainer': document, // DOM element that captures keyboard input
124 'encrypt': false, // Use TLS/SSL/wss encryption
125 'true_color': true, // Request true color pixel data
126 'local_cursor': false, // Request locally rendered cursor
127 'shared': true, // Request shared mode
128 'view_only': false, // Disable client mouse/keyboard
129 'xvp_password_sep': '@', // Separator for XVP password fields
130 'disconnectTimeout': 3, // Time (s) to wait for disconnection
131 'wsProtocols': ['binary', 'base64'], // Protocols to use in the WebSocket connection
132 'repeaterID': '', // [UltraVNC] RepeaterID to connect to
133 'viewportDrag': false, // Move the viewport on mouse drags
134
135 // Callback functions
136 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate, statusMsg): state update/change
137 'onPasswordRequired': function () { }, // onPasswordRequired(rfb): VNC password is required
138 'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received
139 'onBell': function () { }, // onBell(rfb): RFB Bell message received
140 'onFBUReceive': function () { }, // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed
141 'onFBUComplete': function () { }, // onFBUComplete(rfb, fbu): RFB FBU received and processed
142 'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized
143 'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received
144 'onXvpInit': function () { }, // onXvpInit(version): XVP extensions active for this connection
145 });
146
147 // main setup
148 Util.Debug(">> RFB.constructor");
149
150 // populate encHandlers with bound versions
151 Object.keys(RFB.encodingHandlers).forEach(function (encName) {
152 this._encHandlers[encName] = RFB.encodingHandlers[encName].bind(this);
153 }.bind(this));
154
155 // Create lookup tables based on encoding number
156 for (var i = 0; i < this._encodings.length; i++) {
157 this._encHandlers[this._encodings[i][1]] = this._encHandlers[this._encodings[i][0]];
158 this._encNames[this._encodings[i][1]] = this._encodings[i][0];
159 this._encStats[this._encodings[i][1]] = [0, 0];
160 }
161
162 // NB: nothing that needs explicit teardown should be done
163 // before this point, since this can throw an exception
164 try {
165 this._display = new Display({target: this._target});
166 } catch (exc) {
167 Util.Error("Display exception: " + exc);
168 throw exc;
169 }
170
171 this._keyboard = new Keyboard({target: this._focusContainer,
172 onKeyPress: this._handleKeyPress.bind(this)});
173
174 this._mouse = new Mouse({target: this._target,
175 onMouseButton: this._handleMouseButton.bind(this),
176 onMouseMove: this._handleMouseMove.bind(this),
177 notify: this._keyboard.sync.bind(this._keyboard)});
178
179 this._sock = new Websock();
180 this._sock.on('message', this._handle_message.bind(this));
181 this._sock.on('open', function () {
182 if (this._rfb_state === 'connect') {
183 this._updateState('ProtocolVersion', "Starting VNC handshake");
184 } else {
185 this._fail("Got unexpected WebSocket connection");
186 }
187 }.bind(this));
188 this._sock.on('close', function (e) {
189 Util.Warn("WebSocket on-close event");
190 var msg = "";
191 if (e.code) {
192 msg = " (code: " + e.code;
193 if (e.reason) {
194 msg += ", reason: " + e.reason;
195 }
196 msg += ")";
197 }
198 if (this._rfb_state === 'disconnect') {
199 this._updateState('disconnected', 'VNC disconnected' + msg);
200 } else if (this._rfb_state === 'ProtocolVersion') {
201 this._fail('Failed to connect to server' + msg);
202 } else if (this._rfb_state in {'failed': 1, 'disconnected': 1}) {
203 Util.Error("Received onclose while disconnected" + msg);
204 } else {
205 this._fail("Server disconnected" + msg);
206 }
207 this._sock.off('close');
208 }.bind(this));
209 this._sock.on('error', function (e) {
210 Util.Warn("WebSocket on-error event");
211 });
212
213 this._init_vars();
214
215 var rmode = this._display.get_render_mode();
216 if (Websock_native) {
217 Util.Info("Using native WebSockets");
218 this._updateState('loaded', 'noVNC ready: native WebSockets, ' + rmode);
219 } else {
220 Util.Warn("Using web-socket-js bridge. Flash version: " + Util.Flash.version);
221 if (!Util.Flash || Util.Flash.version < 9) {
222 this._cleanupSocket('fatal');
223 throw new Exception("WebSockets or <a href='http://get.adobe.com/flashplayer'>Adobe Flash</a> is required");
224 } else if (document.location.href.substr(0, 7) === 'file://') {
225 this._cleanupSocket('fatal');
226 throw new Exception("'file://' URL is incompatible with Adobe Flash");
227 } else {
228 this._updateState('loaded', 'noVNC ready: WebSockets emulation, ' + rmode);
229 }
230 }
231
232 Util.Debug("<< RFB.constructor");
233 };
234
235 RFB.prototype = {
236 // Public methods
237 connect: function (host, port, password, path) {
238 this._rfb_host = host;
239 this._rfb_port = port;
240 this._rfb_password = (password !== undefined) ? password : "";
241 this._rfb_path = (path !== undefined) ? path : "";
242
243 if (!this._rfb_host || !this._rfb_port) {
244 return this._fail("Must set host and port");
245 }
246
247 this._updateState('connect');
248 },
249
250 disconnect: function () {
251 this._updateState('disconnect', 'Disconnecting');
252 this._sock.off('error');
253 this._sock.off('message');
254 this._sock.off('open');
255 },
256
257 sendPassword: function (passwd) {
258 this._rfb_password = passwd;
259 this._rfb_state = 'Authentication';
260 setTimeout(this._init_msg.bind(this), 1);
261 },
262
263 sendCtrlAltDel: function () {
264 if (this._rfb_state !== 'normal' || this._view_only) { return false; }
265 Util.Info("Sending Ctrl-Alt-Del");
266
267 var arr = [];
268 arr = arr.concat(RFB.messages.keyEvent(XK_Control_L, 1));
269 arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 1));
270 arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 1));
271 arr = arr.concat(RFB.messages.keyEvent(XK_Delete, 0));
272 arr = arr.concat(RFB.messages.keyEvent(XK_Alt_L, 0));
273 arr = arr.concat(RFB.messages.keyEvent(XK_Control_L, 0));
274 this._sock.send(arr);
275 },
276
277 xvpOp: function (ver, op) {
278 if (this._rfb_xvp_ver < ver) { return false; }
279 Util.Info("Sending XVP operation " + op + " (version " + ver + ")");
280 this._sock.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op));
281 return true;
282 },
283
284 xvpShutdown: function () {
285 return this.xvpOp(1, 2);
286 },
287
288 xvpReboot: function () {
289 return this.xvpOp(1, 3);
290 },
291
292 xvpReset: function () {
293 return this.xvpOp(1, 4);
294 },
295
296 // Send a key press. If 'down' is not specified then send a down key
297 // followed by an up key.
298 sendKey: function (code, down) {
299 if (this._rfb_state !== "normal" || this._view_only) { return false; }
300 var arr = [];
301 if (typeof down !== 'undefined') {
302 Util.Info("Sending key code (" + (down ? "down" : "up") + "): " + code);
303 arr = arr.concat(RFB.messages.keyEvent(code, down ? 1 : 0));
304 } else {
305 Util.Info("Sending key code (down + up): " + code);
306 arr = arr.concat(RFB.messages.keyEvent(code, 1));
307 arr = arr.concat(RFB.messages.keyEvent(code, 0));
308 }
309 this._sock.send(arr);
310 },
311
312 clipboardPasteFrom: function (text) {
313 if (this._rfb_state !== 'normal') { return; }
314 this._sock.send(RFB.messages.clientCutText(text));
315 },
316
317 setDesktopSize: function (width, height) {
318 if (this._rfb_state !== "normal") { return; }
319
320 if (this._supportsSetDesktopSize) {
321
322 var arr = [251]; // msg-type
323 arr.push8(0); // padding
324 arr.push16(width); // width
325 arr.push16(height); // height
326
327 arr.push8(1); // number-of-screens
328 arr.push8(0); // padding
329
330 // screen array
331 arr.push32(this._screen_id); // id
332 arr.push16(0); // x-position
333 arr.push16(0); // y-position
334 arr.push16(width); // width
335 arr.push16(height); // height
336 arr.push32(this._screen_flags); // flags
337
338 this._sock.send(arr);
339 }
340 },
341
342
343 // Private methods
344
345 _connect: function () {
346 Util.Debug(">> RFB.connect");
347
348 var uri;
349 if (typeof UsingSocketIO !== 'undefined') {
350 uri = 'http';
351 } else {
352 uri = this._encrypt ? 'wss' : 'ws';
353 }
354
355 uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path;
356 Util.Info("connecting to " + uri);
357
358 this._sock.open(uri, this._wsProtocols);
359
360 Util.Debug("<< RFB.connect");
361 },
362
363 _init_vars: function () {
364 // reset state
365 this._sock.init();
366
367 this._FBU.rects = 0;
368 this._FBU.subrects = 0; // RRE and HEXTILE
369 this._FBU.lines = 0; // RAW
370 this._FBU.tiles = 0; // HEXTILE
371 this._FBU.zlibs = []; // TIGHT zlib encoders
372 this._mouse_buttonMask = 0;
373 this._mouse_arr = [];
374 this._rfb_tightvnc = false;
375
376 // Clear the per connection encoding stats
377 var i;
378 for (i = 0; i < this._encodings.length; i++) {
379 this._encStats[this._encodings[i][1]][0] = 0;
380 }
381
382 for (i = 0; i < 4; i++) {
383 this._FBU.zlibs[i] = new TINF();
384 this._FBU.zlibs[i].init();
385 }
386 },
387
388 _print_stats: function () {
389 Util.Info("Encoding stats for this connection:");
390 var i, s;
391 for (i = 0; i < this._encodings.length; i++) {
392 s = this._encStats[this._encodings[i][1]];
393 if (s[0] + s[1] > 0) {
394 Util.Info(" " + this._encodings[i][0] + ": " + s[0] + " rects");
395 }
396 }
397
398 Util.Info("Encoding stats since page load:");
399 for (i = 0; i < this._encodings.length; i++) {
400 s = this._encStats[this._encodings[i][1]];
401 Util.Info(" " + this._encodings[i][0] + ": " + s[1] + " rects");
402 }
403 },
404
405 _cleanupSocket: function (state) {
406 if (this._sendTimer) {
407 clearInterval(this._sendTimer);
408 this._sendTimer = null;
409 }
410
411 if (this._msgTimer) {
412 clearInterval(this._msgTimer);
413 this._msgTimer = null;
414 }
415
416 if (this._display && this._display.get_context()) {
417 this._keyboard.ungrab();
418 this._mouse.ungrab();
419 if (state !== 'connect' && state !== 'loaded') {
420 this._display.defaultCursor();
421 }
422 if (Util.get_logging() !== 'debug' || state === 'loaded') {
423 // Show noVNC logo on load and when disconnected, unless in
424 // debug mode
425 this._display.clear();
426 }
427 }
428
429 this._sock.close();
430 },
431
432 /*
433 * Page states:
434 * loaded - page load, equivalent to disconnected
435 * disconnected - idle state
436 * connect - starting to connect (to ProtocolVersion)
437 * normal - connected
438 * disconnect - starting to disconnect
439 * failed - abnormal disconnect
440 * fatal - failed to load page, or fatal error
441 *
442 * RFB protocol initialization states:
443 * ProtocolVersion
444 * Security
445 * Authentication
446 * password - waiting for password, not part of RFB
447 * SecurityResult
448 * ClientInitialization - not triggered by server message
449 * ServerInitialization (to normal)
450 */
451 _updateState: function (state, statusMsg) {
452 var oldstate = this._rfb_state;
453
454 if (state === oldstate) {
455 // Already here, ignore
456 Util.Debug("Already in state '" + state + "', ignoring");
457 }
458
459 /*
460 * These are disconnected states. A previous connect may
461 * asynchronously cause a connection so make sure we are closed.
462 */
463 if (state in {'disconnected': 1, 'loaded': 1, 'connect': 1,
464 'disconnect': 1, 'failed': 1, 'fatal': 1}) {
465 this._cleanupSocket(state);
466 }
467
468 if (oldstate === 'fatal') {
469 Util.Error('Fatal error, cannot continue');
470 }
471
472 var cmsg = typeof(statusMsg) !== 'undefined' ? (" Msg: " + statusMsg) : "";
473 var fullmsg = "New state '" + state + "', was '" + oldstate + "'." + cmsg;
474 if (state === 'failed' || state === 'fatal') {
475 Util.Error(cmsg);
476 } else {
477 Util.Warn(cmsg);
478 }
479
480 if (oldstate === 'failed' && state === 'disconnected') {
481 // do disconnect action, but stay in failed state
482 this._rfb_state = 'failed';
483 } else {
484 this._rfb_state = state;
485 }
486
487 if (this._disconnTimer && this._rfb_state !== 'disconnect') {
488 Util.Debug("Clearing disconnect timer");
489 clearTimeout(this._disconnTimer);
490 this._disconnTimer = null;
491 this._sock.off('close'); // make sure we don't get a double event
492 }
493
494 switch (state) {
495 case 'normal':
496 if (oldstate === 'disconnected' || oldstate === 'failed') {
497 Util.Error("Invalid transition from 'disconnected' or 'failed' to 'normal'");
498 }
499 break;
500
501 case 'connect':
502 this._init_vars();
503 this._connect();
504 // WebSocket.onopen transitions to 'ProtocolVersion'
505 break;
506
507 case 'disconnect':
508 this._disconnTimer = setTimeout(function () {
509 this._fail("Disconnect timeout");
510 }.bind(this), this._disconnectTimeout * 1000);
511
512 this._print_stats();
513
514 // WebSocket.onclose transitions to 'disconnected'
515 break;
516
517 case 'failed':
518 if (oldstate === 'disconnected') {
519 Util.Error("Invalid transition from 'disconnected' to 'failed'");
520 } else if (oldstate === 'normal') {
521 Util.Error("Error while connected.");
522 } else if (oldstate === 'init') {
523 Util.Error("Error while initializing.");
524 }
525
526 // Make sure we transition to disconnected
527 setTimeout(function () {
528 this._updateState('disconnected');
529 }.bind(this), 50);
530
531 break;
532
533 default:
534 // No state change action to take
535 }
536
537 if (oldstate === 'failed' && state === 'disconnected') {
538 this._onUpdateState(this, state, oldstate);
539 } else {
540 this._onUpdateState(this, state, oldstate, statusMsg);
541 }
542 },
543
544 _fail: function (msg) {
545 this._updateState('failed', msg);
546 return false;
547 },
548
549 _handle_message: function () {
550 if (this._sock.rQlen() === 0) {
551 Util.Warn("handle_message called on an empty receive queue");
552 return;
553 }
554
555 switch (this._rfb_state) {
556 case 'disconnected':
557 case 'failed':
558 Util.Error("Got data while disconnected");
559 break;
560 case 'normal':
561 if (this._normal_msg() && this._sock.rQlen() > 0) {
562 // true means we can continue processing
563 // Give other events a chance to run
564 if (this._msgTimer === null) {
565 Util.Debug("More data to process, creating timer");
566 this._msgTimer = setTimeout(function () {
567 this._msgTimer = null;
568 this._handle_message();
569 }.bind(this), 10);
570 } else {
571 Util.Debug("More data to process, existing timer");
572 }
573 }
574 break;
575 default:
576 this._init_msg();
577 break;
578 }
579 },
580
581 _checkEvents: function () {
582 if (this._rfb_state === 'normal' && !this._viewportDragging && this._mouse_arr.length > 0) {
583 this._sock.send(this._mouse_arr);
584 this._mouse_arr = [];
585 }
586 },
587
588 _handleKeyPress: function (keysym, down) {
589 if (this._view_only) { return; } // View only, skip keyboard, events
590 this._sock.send(RFB.messages.keyEvent(keysym, down));
591 },
592
593 _handleMouseButton: function (x, y, down, bmask) {
594 if (down) {
595 this._mouse_buttonMask |= bmask;
596 } else {
597 this._mouse_buttonMask ^= bmask;
598 }
599
600 if (this._viewportDrag) {
601 if (down && !this._viewportDragging) {
602 this._viewportDragging = true;
603 this._viewportDragPos = {'x': x, 'y': y};
604
605 // Skip sending mouse events
606 return;
607 } else {
608 this._viewportDragging = false;
609 }
610 }
611
612 if (this._view_only) { return; } // View only, skip mouse events
613
614 this._mouse_arr = this._mouse_arr.concat(
615 RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask));
616 this._sock.send(this._mouse_arr);
617 this._mouse_arr = [];
618 },
619
620 _handleMouseMove: function (x, y) {
621 if (this._viewportDragging) {
622 var deltaX = this._viewportDragPos.x - x;
623 var deltaY = this._viewportDragPos.y - y;
624 this._viewportDragPos = {'x': x, 'y': y};
625
626 this._display.viewportChangePos(deltaX, deltaY);
627
628 // Skip sending mouse events
629 return;
630 }
631
632 if (this._view_only) { return; } // View only, skip mouse events
633
634 this._mouse_arr = this._mouse_arr.concat(
635 RFB.messages.pointerEvent(this._display.absX(x), this._display.absY(y), this._mouse_buttonMask));
636
637 this._checkEvents();
638 },
639
640 // Message Handlers
641
642 _negotiate_protocol_version: function () {
643 if (this._sock.rQlen() < 12) {
644 return this._fail("Incomplete protocol version");
645 }
646
647 var sversion = this._sock.rQshiftStr(12).substr(4, 7);
648 Util.Info("Server ProtocolVersion: " + sversion);
649 var is_repeater = 0;
650 switch (sversion) {
651 case "000.000": // UltraVNC repeater
652 is_repeater = 1;
653 break;
654 case "003.003":
655 case "003.006": // UltraVNC
656 case "003.889": // Apple Remote Desktop
657 this._rfb_version = 3.3;
658 break;
659 case "003.007":
660 this._rfb_version = 3.7;
661 break;
662 case "003.008":
663 case "004.000": // Intel AMT KVM
664 case "004.001": // RealVNC 4.6
665 this._rfb_version = 3.8;
666 break;
667 default:
668 return this._fail("Invalid server version " + sversion);
669 }
670
671 if (is_repeater) {
672 var repeaterID = this._repeaterID;
673 while (repeaterID.length < 250) {
674 repeaterID += "\0";
675 }
676 this._sock.send_string(repeaterID);
677 return true;
678 }
679
680 if (this._rfb_version > this._rfb_max_version) {
681 this._rfb_version = this._rfb_max_version;
682 }
683
684 // Send updates either at a rate of 1 update per 50ms, or
685 // whatever slower rate the network can handle
686 this._sendTimer = setInterval(this._sock.flush.bind(this._sock), 50);
687
688 var cversion = "00" + parseInt(this._rfb_version, 10) +
689 ".00" + ((this._rfb_version * 10) % 10);
690 this._sock.send_string("RFB " + cversion + "\n");
691 this._updateState('Security', 'Sent ProtocolVersion: ' + cversion);
692 },
693
694 _negotiate_security: function () {
695 if (this._rfb_version >= 3.7) {
696 // Server sends supported list, client decides
697 var num_types = this._sock.rQshift8();
698 if (this._sock.rQwait("security type", num_types, 1)) { return false; }
699
700 if (num_types === 0) {
701 var strlen = this._sock.rQshift32();
702 var reason = this._sock.rQshiftStr(strlen);
703 return this._fail("Security failure: " + reason);
704 }
705
706 this._rfb_auth_scheme = 0;
707 var types = this._sock.rQshiftBytes(num_types);
708 Util.Debug("Server security types: " + types);
709 for (var i = 0; i < types.length; i++) {
710 if (types[i] > this._rfb_auth_scheme && (types[i] <= 16 || types[i] == 22)) {
711 this._rfb_auth_scheme = types[i];
712 }
713 }
714
715 if (this._rfb_auth_scheme === 0) {
716 return this._fail("Unsupported security types: " + types);
717 }
718
719 this._sock.send([this._rfb_auth_scheme]);
720 } else {
721 // Server decides
722 if (this._sock.rQwait("security scheme", 4)) { return false; }
723 this._rfb_auth_scheme = this._sock.rQshift32();
724 }
725
726 this._updateState('Authentication', 'Authenticating using scheme: ' + this._rfb_auth_scheme);
727 return this._init_msg(); // jump to authentication
728 },
729
730 // authentication
731 _negotiate_xvp_auth: function () {
732 var xvp_sep = this._xvp_password_sep;
733 var xvp_auth = this._rfb_password.split(xvp_sep);
734 if (xvp_auth.length < 3) {
735 this._updateState('password', 'XVP credentials required (user' + xvp_sep +
736 'target' + xvp_sep + 'password) -- got only ' + this._rfb_password);
737 this._onPasswordRequired(this);
738 return false;
739 }
740
741 var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) +
742 String.fromCharCode(xvp_auth[1].length) +
743 xvp_auth[0] +
744 xvp_auth[1];
745 this._sock.send_string(xvp_auth_str);
746 this._rfb_password = xvp_auth.slice(2).join(xvp_sep);
747 this._rfb_auth_scheme = 2;
748 return this._negotiate_authentication();
749 },
750
751 _negotiate_std_vnc_auth: function () {
752 if (this._rfb_password.length === 0) {
753 // Notify via both callbacks since it's kind of
754 // an RFB state change and a UI interface issue
755 this._updateState('password', "Password Required");
756 this._onPasswordRequired(this);
757 }
758
759 if (this._sock.rQwait("auth challenge", 16)) { return false; }
760
761 var challenge = this._sock.rQshiftBytes(16);
762 var response = RFB.genDES(this._rfb_password, challenge);
763 this._sock.send(response);
764 this._updateState("SecurityResult");
765 return true;
766 },
767
768 _negotiate_tight_tunnels: function (numTunnels) {
769 var clientSupportedTunnelTypes = {
770 0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
771 };
772 var serverSupportedTunnelTypes = {};
773 // receive tunnel capabilities
774 for (var i = 0; i < numTunnels; i++) {
775 var cap_code = this._sock.rQshift32();
776 var cap_vendor = this._sock.rQshiftStr(4);
777 var cap_signature = this._sock.rQshiftStr(8);
778 serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature };
779 }
780
781 // choose the notunnel type
782 if (serverSupportedTunnelTypes[0]) {
783 if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
784 serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
785 return this._fail("Client's tunnel type had the incorrect vendor or signature");
786 }
787 this._sock.send([0, 0, 0, 0]); // use NOTUNNEL
788 return false; // wait until we receive the sub auth count to continue
789 } else {
790 return this._fail("Server wanted tunnels, but doesn't support the notunnel type");
791 }
792 },
793
794 _negotiate_tight_auth: function () {
795 if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation
796 if (this._sock.rQwait("num tunnels", 4)) { return false; }
797 var numTunnels = this._sock.rQshift32();
798 if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
799
800 this._rfb_tightvnc = true;
801
802 if (numTunnels > 0) {
803 this._negotiate_tight_tunnels(numTunnels);
804 return false; // wait until we receive the sub auth to continue
805 }
806 }
807
808 // second pass, do the sub-auth negotiation
809 if (this._sock.rQwait("sub auth count", 4)) { return false; }
810 var subAuthCount = this._sock.rQshift32();
811 if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
812
813 var clientSupportedTypes = {
814 'STDVNOAUTH__': 1,
815 'STDVVNCAUTH_': 2
816 };
817
818 var serverSupportedTypes = [];
819
820 for (var i = 0; i < subAuthCount; i++) {
821 var capNum = this._sock.rQshift32();
822 var capabilities = this._sock.rQshiftStr(12);
823 serverSupportedTypes.push(capabilities);
824 }
825
826 for (var authType in clientSupportedTypes) {
827 if (serverSupportedTypes.indexOf(authType) != -1) {
828 this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
829
830 switch (authType) {
831 case 'STDVNOAUTH__': // no auth
832 this._updateState('SecurityResult');
833 return true;
834 case 'STDVVNCAUTH_': // VNC auth
835 this._rfb_auth_scheme = 2;
836 return this._init_msg();
837 default:
838 return this._fail("Unsupported tiny auth scheme: " + authType);
839 }
840 }
841 }
842
843 this._fail("No supported sub-auth types!");
844 },
845
846 _negotiate_authentication: function () {
847 switch (this._rfb_auth_scheme) {
848 case 0: // connection failed
849 if (this._sock.rQwait("auth reason", 4)) { return false; }
850 var strlen = this._sock.rQshift32();
851 var reason = this._sock.rQshiftStr(strlen);
852 return this._fail("Auth failure: " + reason);
853
854 case 1: // no auth
855 if (this._rfb_version >= 3.8) {
856 this._updateState('SecurityResult');
857 return true;
858 }
859 this._updateState('ClientInitialisation', "No auth required");
860 return this._init_msg();
861
862 case 22: // XVP auth
863 return this._negotiate_xvp_auth();
864
865 case 2: // VNC authentication
866 return this._negotiate_std_vnc_auth();
867
868 case 16: // TightVNC Security Type
869 return this._negotiate_tight_auth();
870
871 default:
872 return this._fail("Unsupported auth scheme: " + this._rfb_auth_scheme);
873 }
874 },
875
876 _handle_security_result: function () {
877 if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
878 switch (this._sock.rQshift32()) {
879 case 0: // OK
880 this._updateState('ClientInitialisation', 'Authentication OK');
881 return this._init_msg();
882 case 1: // failed
883 if (this._rfb_version >= 3.8) {
884 var length = this._sock.rQshift32();
885 if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; }
886 var reason = this._sock.rQshiftStr(length);
887 return this._fail(reason);
888 } else {
889 return this._fail("Authentication failure");
890 }
891 return false;
892 case 2:
893 return this._fail("Too many auth attempts");
894 }
895 },
896
897 _negotiate_server_init: function () {
898 if (this._sock.rQwait("server initialization", 24)) { return false; }
899
900 /* Screen size */
901 this._fb_width = this._sock.rQshift16();
902 this._fb_height = this._sock.rQshift16();
903
904 /* PIXEL_FORMAT */
905 var bpp = this._sock.rQshift8();
906 var depth = this._sock.rQshift8();
907 var big_endian = this._sock.rQshift8();
908 var true_color = this._sock.rQshift8();
909
910 var red_max = this._sock.rQshift16();
911 var green_max = this._sock.rQshift16();
912 var blue_max = this._sock.rQshift16();
913 var red_shift = this._sock.rQshift8();
914 var green_shift = this._sock.rQshift8();
915 var blue_shift = this._sock.rQshift8();
916 this._sock.rQskipBytes(3); // padding
917
918 // NB(directxman12): we don't want to call any callbacks or print messages until
919 // *after* we're past the point where we could backtrack
920
921 /* Connection name/title */
922 var name_length = this._sock.rQshift32();
923 if (this._sock.rQwait('server init name', name_length, 24)) { return false; }
924 this._fb_name = Util.decodeUTF8(this._sock.rQshiftStr(name_length));
925
926 if (this._rfb_tightvnc) {
927 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; }
928 // In TightVNC mode, ServerInit message is extended
929 var numServerMessages = this._sock.rQshift16();
930 var numClientMessages = this._sock.rQshift16();
931 var numEncodings = this._sock.rQshift16();
932 this._sock.rQskipBytes(2); // padding
933
934 var totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
935 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; }
936
937 var i;
938 for (i = 0; i < numServerMessages; i++) {
939 var srvMsg = this._sock.rQshiftStr(16);
940 }
941
942 for (i = 0; i < numClientMessages; i++) {
943 var clientMsg = this._sock.rQshiftStr(16);
944 }
945
946 for (i = 0; i < numEncodings; i++) {
947 var encoding = this._sock.rQshiftStr(16);
948 }
949 }
950
951 // NB(directxman12): these are down here so that we don't run them multiple times
952 // if we backtrack
953 Util.Info("Screen: " + this._fb_width + "x" + this._fb_height +
954 ", bpp: " + bpp + ", depth: " + depth +
955 ", big_endian: " + big_endian +
956 ", true_color: " + true_color +
957 ", red_max: " + red_max +
958 ", green_max: " + green_max +
959 ", blue_max: " + blue_max +
960 ", red_shift: " + red_shift +
961 ", green_shift: " + green_shift +
962 ", blue_shift: " + blue_shift);
963
964 if (big_endian !== 0) {
965 Util.Warn("Server native endian is not little endian");
966 }
967
968 if (red_shift !== 16) {
969 Util.Warn("Server native red-shift is not 16");
970 }
971
972 if (blue_shift !== 0) {
973 Util.Warn("Server native blue-shift is not 0");
974 }
975
976 // we're past the point where we could backtrack, so it's safe to call this
977 this._onDesktopName(this, this._fb_name);
978
979 if (this._true_color && this._fb_name === "Intel(r) AMT KVM") {
980 Util.Warn("Intel AMT KVM only supports 8/16 bit depths. Disabling true color");
981 this._true_color = false;
982 }
983
984 this._display.set_true_color(this._true_color);
985 this._display.resize(this._fb_width, this._fb_height);
986 this._onFBResize(this, this._fb_width, this._fb_height);
987 this._keyboard.grab();
988 this._mouse.grab();
989
990 if (this._true_color) {
991 this._fb_Bpp = 4;
992 this._fb_depth = 3;
993 } else {
994 this._fb_Bpp = 1;
995 this._fb_depth = 1;
996 }
997
998 var response = RFB.messages.pixelFormat(this._fb_Bpp, this._fb_depth, this._true_color);
999 response = response.concat(
1000 RFB.messages.clientEncodings(this._encodings, this._local_cursor, this._true_color));
1001 response = response.concat(
1002 RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(),
1003 this._fb_width, this._fb_height));
1004
1005 this._timing.fbu_rt_start = (new Date()).getTime();
1006 this._timing.pixels = 0;
1007 this._sock.send(response);
1008
1009 this._checkEvents();
1010
1011 if (this._encrypt) {
1012 this._updateState('normal', 'Connected (encrypted) to: ' + this._fb_name);
1013 } else {
1014 this._updateState('normal', 'Connected (unencrypted) to: ' + this._fb_name);
1015 }
1016 },
1017
1018 _init_msg: function () {
1019 switch (this._rfb_state) {
1020 case 'ProtocolVersion':
1021 return this._negotiate_protocol_version();
1022
1023 case 'Security':
1024 return this._negotiate_security();
1025
1026 case 'Authentication':
1027 return this._negotiate_authentication();
1028
1029 case 'SecurityResult':
1030 return this._handle_security_result();
1031
1032 case 'ClientInitialisation':
1033 this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
1034 this._updateState('ServerInitialisation', "Authentication OK");
1035 return true;
1036
1037 case 'ServerInitialisation':
1038 return this._negotiate_server_init();
1039 }
1040 },
1041
1042 _handle_set_colour_map_msg: function () {
1043 Util.Debug("SetColorMapEntries");
1044 this._sock.rQskip8(); // Padding
1045
1046 var first_colour = this._sock.rQshift16();
1047 var num_colours = this._sock.rQshift16();
1048 if (this._sock.rQwait('SetColorMapEntries', num_colours * 6, 6)) { return false; }
1049
1050 for (var c = 0; c < num_colours; c++) {
1051 var red = parseInt(this._sock.rQshift16() / 256, 10);
1052 var green = parseInt(this._sock.rQshift16() / 256, 10);
1053 var blue = parseInt(this._sock.rQshift16() / 256, 10);
1054 this._display.set_colourMap([blue, green, red], first_colour + c);
1055 }
1056 Util.Debug("colourMap: " + this._display.get_colourMap());
1057 Util.Info("Registered " + num_colours + " colourMap entries");
1058
1059 return true;
1060 },
1061
1062 _handle_server_cut_text: function () {
1063 Util.Debug("ServerCutText");
1064 if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
1065 this._sock.rQskipBytes(3); // Padding
1066 var length = this._sock.rQshift32();
1067 if (this._sock.rQwait("ServerCutText", length, 8)) { return false; }
1068
1069 var text = this._sock.rQshiftStr(length);
1070 this._onClipboard(this, text);
1071
1072 return true;
1073 },
1074
1075 _handle_xvp_msg: function () {
1076 if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
1077 this._sock.rQskip8(); // Padding
1078 var xvp_ver = this._sock.rQshift8();
1079 var xvp_msg = this._sock.rQshift8();
1080
1081 switch (xvp_msg) {
1082 case 0: // XVP_FAIL
1083 this._updateState(this._rfb_state, "Operation Failed");
1084 break;
1085 case 1: // XVP_INIT
1086 this._rfb_xvp_ver = xvp_ver;
1087 Util.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")");
1088 this._onXvpInit(this._rfb_xvp_ver);
1089 break;
1090 default:
1091 this._fail("Disconnected: illegal server XVP message " + xvp_msg);
1092 break;
1093 }
1094
1095 return true;
1096 },
1097
1098 _normal_msg: function () {
1099 var msg_type;
1100
1101 if (this._FBU.rects > 0) {
1102 msg_type = 0;
1103 } else {
1104 msg_type = this._sock.rQshift8();
1105 }
1106
1107 switch (msg_type) {
1108 case 0: // FramebufferUpdate
1109 var ret = this._framebufferUpdate();
1110 if (ret) {
1111 this._sock.send(RFB.messages.fbUpdateRequests(this._display.getCleanDirtyReset(),
1112 this._fb_width, this._fb_height));
1113 }
1114 return ret;
1115
1116 case 1: // SetColorMapEntries
1117 return this._handle_set_colour_map_msg();
1118
1119 case 2: // Bell
1120 Util.Debug("Bell");
1121 this._onBell(this);
1122 return true;
1123
1124 case 3: // ServerCutText
1125 return this._handle_server_cut_text();
1126
1127 case 250: // XVP
1128 return this._handle_xvp_msg();
1129
1130 default:
1131 this._fail("Disconnected: illegal server message type " + msg_type);
1132 Util.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
1133 return true;
1134 }
1135 },
1136
1137 _framebufferUpdate: function () {
1138 var ret = true;
1139 var now;
1140
1141 if (this._FBU.rects === 0) {
1142 if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
1143 this._sock.rQskip8(); // Padding
1144 this._FBU.rects = this._sock.rQshift16();
1145 this._FBU.bytes = 0;
1146 this._timing.cur_fbu = 0;
1147 if (this._timing.fbu_rt_start > 0) {
1148 now = (new Date()).getTime();
1149 Util.Info("First FBU latency: " + (now - this._timing.fbu_rt_start));
1150 }
1151 }
1152
1153 while (this._FBU.rects > 0) {
1154 if (this._rfb_state !== "normal") { return false; }
1155
1156 if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; }
1157 if (this._FBU.bytes === 0) {
1158 if (this._sock.rQwait("rect header", 12)) { return false; }
1159 /* New FramebufferUpdate */
1160
1161 var hdr = this._sock.rQshiftBytes(12);
1162 this._FBU.x = (hdr[0] << 8) + hdr[1];
1163 this._FBU.y = (hdr[2] << 8) + hdr[3];
1164 this._FBU.width = (hdr[4] << 8) + hdr[5];
1165 this._FBU.height = (hdr[6] << 8) + hdr[7];
1166 this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
1167 (hdr[10] << 8) + hdr[11], 10);
1168
1169 this._onFBUReceive(this,
1170 {'x': this._FBU.x, 'y': this._FBU.y,
1171 'width': this._FBU.width, 'height': this._FBU.height,
1172 'encoding': this._FBU.encoding,
1173 'encodingName': this._encNames[this._FBU.encoding]});
1174
1175 if (!this._encNames[this._FBU.encoding]) {
1176 this._fail("Disconnected: unsupported encoding " +
1177 this._FBU.encoding);
1178 return false;
1179 }
1180 }
1181
1182 this._timing.last_fbu = (new Date()).getTime();
1183
1184 ret = this._encHandlers[this._FBU.encoding]();
1185
1186 now = (new Date()).getTime();
1187 this._timing.cur_fbu += (now - this._timing.last_fbu);
1188
1189 if (ret) {
1190 this._encStats[this._FBU.encoding][0]++;
1191 this._encStats[this._FBU.encoding][1]++;
1192 this._timing.pixels += this._FBU.width * this._FBU.height;
1193 }
1194
1195 if (this._timing.pixels >= (this._fb_width * this._fb_height)) {
1196 if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) ||
1197 this._timing.fbu_rt_start > 0) {
1198 this._timing.full_fbu_total += this._timing.cur_fbu;
1199 this._timing.full_fbu_cnt++;
1200 Util.Info("Timing of full FBU, curr: " +
1201 this._timing.cur_fbu + ", total: " +
1202 this._timing.full_fbu_total + ", cnt: " +
1203 this._timing.full_fbu_cnt + ", avg: " +
1204 (this._timing.full_fbu_total / this._timing.full_fbu_cnt));
1205 }
1206
1207 if (this._timing.fbu_rt_start > 0) {
1208 var fbu_rt_diff = now - this._timing.fbu_rt_start;
1209 this._timing.fbu_rt_total += fbu_rt_diff;
1210 this._timing.fbu_rt_cnt++;
1211 Util.Info("full FBU round-trip, cur: " +
1212 fbu_rt_diff + ", total: " +
1213 this._timing.fbu_rt_total + ", cnt: " +
1214 this._timing.fbu_rt_cnt + ", avg: " +
1215 (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt));
1216 this._timing.fbu_rt_start = 0;
1217 }
1218 }
1219
1220 if (!ret) { return ret; } // need more data
1221 }
1222
1223 this._onFBUComplete(this,
1224 {'x': this._FBU.x, 'y': this._FBU.y,
1225 'width': this._FBU.width, 'height': this._FBU.height,
1226 'encoding': this._FBU.encoding,
1227 'encodingName': this._encNames[this._FBU.encoding]});
1228
1229 return true; // We finished this FBU
1230 },
1231 };
1232
1233 Util.make_properties(RFB, [
1234 ['target', 'wo', 'dom'], // VNC display rendering Canvas object
1235 ['focusContainer', 'wo', 'dom'], // DOM element that captures keyboard input
1236 ['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption
1237 ['true_color', 'rw', 'bool'], // Request true color pixel data
1238 ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor
1239 ['shared', 'rw', 'bool'], // Request shared mode
1240 ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard
1241 ['xvp_password_sep', 'rw', 'str'], // Separator for XVP password fields
1242 ['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection
1243 ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection
1244 ['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to
1245 ['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags
1246
1247 // Callback functions
1248 ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate, statusMsg): RFB state update/change
1249 ['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb): VNC password is required
1250 ['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received
1251 ['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received
1252 ['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed
1253 ['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed
1254 ['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized
1255 ['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received
1256 ['onXvpInit', 'rw', 'func'], // onXvpInit(version): XVP extensions active for this connection
1257 ]);
1258
1259 RFB.prototype.set_local_cursor = function (cursor) {
1260 if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) {
1261 this._local_cursor = false;
1262 this._display.disableLocalCursor(); //Only show server-side cursor
1263 } else {
1264 if (this._display.get_cursor_uri()) {
1265 this._local_cursor = true;
1266 } else {
1267 Util.Warn("Browser does not support local cursor");
1268 }
1269 }
1270 };
1271
1272 RFB.prototype.get_display = function () { return this._display; };
1273 RFB.prototype.get_keyboard = function () { return this._keyboard; };
1274 RFB.prototype.get_mouse = function () { return this._mouse; };
1275
1276 // Class Methods
1277 RFB.messages = {
1278 keyEvent: function (keysym, down) {
1279 var arr = [4];
1280 arr.push8(down);
1281 arr.push16(0);
1282 arr.push32(keysym);
1283 return arr;
1284 },
1285
1286 pointerEvent: function (x, y, mask) {
1287 var arr = [5]; // msg-type
1288 arr.push8(mask);
1289 arr.push16(x);
1290 arr.push16(y);
1291 return arr;
1292 },
1293
1294 // TODO(directxman12): make this unicode compatible?
1295 clientCutText: function (text) {
1296 var arr = [6]; // msg-type
1297 arr.push8(0); // padding
1298 arr.push8(0); // padding
1299 arr.push8(0); // padding
1300 arr.push32(text.length);
1301 var n = text.length;
1302 for (var i = 0; i < n; i++) {
1303 arr.push(text.charCodeAt(i));
1304 }
1305
1306 return arr;
1307 },
1308
1309 pixelFormat: function (bpp, depth, true_color) {
1310 var arr = [0]; // msg-type
1311 arr.push8(0); // padding
1312 arr.push8(0); // padding
1313 arr.push8(0); // padding
1314
1315 arr.push8(bpp * 8); // bits-per-pixel
1316 arr.push8(depth * 8); // depth
1317 arr.push8(0); // little-endian
1318 arr.push8(true_color ? 1 : 0); // true-color
1319
1320 arr.push16(255); // red-max
1321 arr.push16(255); // green-max
1322 arr.push16(255); // blue-max
1323 arr.push8(16); // red-shift
1324 arr.push8(8); // green-shift
1325 arr.push8(0); // blue-shift
1326
1327 arr.push8(0); // padding
1328 arr.push8(0); // padding
1329 arr.push8(0); // padding
1330 return arr;
1331 },
1332
1333 clientEncodings: function (encodings, local_cursor, true_color) {
1334 var i, encList = [];
1335
1336 for (i = 0; i < encodings.length; i++) {
1337 if (encodings[i][0] === "Cursor" && !local_cursor) {
1338 Util.Debug("Skipping Cursor pseudo-encoding");
1339 } else if (encodings[i][0] === "TIGHT" && !true_color) {
1340 // TODO: remove this when we have tight+non-true-color
1341 Util.Warn("Skipping tight as it is only supported with true color");
1342 } else {
1343 encList.push(encodings[i][1]);
1344 }
1345 }
1346
1347 var arr = [2]; // msg-type
1348 arr.push8(0); // padding
1349
1350 arr.push16(encList.length); // encoding count
1351 for (i = 0; i < encList.length; i++) {
1352 arr.push32(encList[i]);
1353 }
1354
1355 return arr;
1356 },
1357
1358 fbUpdateRequests: function (cleanDirty, fb_width, fb_height) {
1359 var arr = [];
1360
1361 var cb = cleanDirty.cleanBox;
1362 var w, h;
1363 if (cb.w > 0 && cb.h > 0) {
1364 w = typeof cb.w === "undefined" ? fb_width : cb.w;
1365 h = typeof cb.h === "undefined" ? fb_height : cb.h;
1366 // Request incremental for clean box
1367 arr = arr.concat(RFB.messages.fbUpdateRequest(1, cb.x, cb.y, w, h));
1368 }
1369
1370 for (var i = 0; i < cleanDirty.dirtyBoxes.length; i++) {
1371 var db = cleanDirty.dirtyBoxes[i];
1372 // Force all (non-incremental) for dirty box
1373 w = typeof db.w === "undefined" ? fb_width : db.w;
1374 h = typeof db.h === "undefined" ? fb_height : db.h;
1375 arr = arr.concat(RFB.messages.fbUpdateRequest(0, db.x, db.y, w, h));
1376 }
1377
1378 return arr;
1379 },
1380
1381 fbUpdateRequest: function (incremental, x, y, w, h) {
1382 if (typeof(x) === "undefined") { x = 0; }
1383 if (typeof(y) === "undefined") { y = 0; }
1384
1385 var arr = [3]; // msg-type
1386 arr.push8(incremental);
1387 arr.push16(x);
1388 arr.push16(y);
1389 arr.push16(w);
1390 arr.push16(h);
1391
1392 return arr;
1393 }
1394 };
1395
1396 RFB.genDES = function (password, challenge) {
1397 var passwd = [];
1398 for (var i = 0; i < password.length; i++) {
1399 passwd.push(password.charCodeAt(i));
1400 }
1401 return (new DES(passwd)).encrypt(challenge);
1402 };
1403
1404 RFB.extract_data_uri = function (arr) {
1405 return ";base64," + Base64.encode(arr);
1406 };
1407
1408 RFB.encodingHandlers = {
1409 RAW: function () {
1410 if (this._FBU.lines === 0) {
1411 this._FBU.lines = this._FBU.height;
1412 }
1413
1414 this._FBU.bytes = this._FBU.width * this._fb_Bpp; // at least a line
1415 if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; }
1416 var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines);
1417 var curr_height = Math.min(this._FBU.lines,
1418 Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp)));
1419 this._display.blitImage(this._FBU.x, cur_y, this._FBU.width,
1420 curr_height, this._sock.get_rQ(),
1421 this._sock.get_rQi());
1422 this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp);
1423 this._FBU.lines -= curr_height;
1424
1425 if (this._FBU.lines > 0) {
1426 this._FBU.bytes = this._FBU.width * this._fb_Bpp; // At least another line
1427 } else {
1428 this._FBU.rects--;
1429 this._FBU.bytes = 0;
1430 }
1431
1432 return true;
1433 },
1434
1435 COPYRECT: function () {
1436 this._FBU.bytes = 4;
1437 if (this._sock.rQwait("COPYRECT", 4)) { return false; }
1438 this._display.renderQ_push({
1439 'type': 'copy',
1440 'old_x': this._sock.rQshift16(),
1441 'old_y': this._sock.rQshift16(),
1442 'x': this._FBU.x,
1443 'y': this._FBU.y,
1444 'width': this._FBU.width,
1445 'height': this._FBU.height
1446 });
1447 this._FBU.rects--;
1448 this._FBU.bytes = 0;
1449 return true;
1450 },
1451
1452 RRE: function () {
1453 var color;
1454 if (this._FBU.subrects === 0) {
1455 this._FBU.bytes = 4 + this._fb_Bpp;
1456 if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; }
1457 this._FBU.subrects = this._sock.rQshift32();
1458 color = this._sock.rQshiftBytes(this._fb_Bpp); // Background
1459 this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color);
1460 }
1461
1462 while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) {
1463 color = this._sock.rQshiftBytes(this._fb_Bpp);
1464 var x = this._sock.rQshift16();
1465 var y = this._sock.rQshift16();
1466 var width = this._sock.rQshift16();
1467 var height = this._sock.rQshift16();
1468 this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color);
1469 this._FBU.subrects--;
1470 }
1471
1472 if (this._FBU.subrects > 0) {
1473 var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects);
1474 this._FBU.bytes = (this._fb_Bpp + 8) * chunk;
1475 } else {
1476 this._FBU.rects--;
1477 this._FBU.bytes = 0;
1478 }
1479
1480 return true;
1481 },
1482
1483 HEXTILE: function () {
1484 var rQ = this._sock.get_rQ();
1485 var rQi = this._sock.get_rQi();
1486
1487 if (this._FBU.tiles === 0) {
1488 this._FBU.tiles_x = Math.ceil(this._FBU.width / 16);
1489 this._FBU.tiles_y = Math.ceil(this._FBU.height / 16);
1490 this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y;
1491 this._FBU.tiles = this._FBU.total_tiles;
1492 }
1493
1494 while (this._FBU.tiles > 0) {
1495 this._FBU.bytes = 1;
1496 if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; }
1497 var subencoding = rQ[rQi]; // Peek
1498 if (subencoding > 30) { // Raw
1499 this._fail("Disconnected: illegal hextile subencoding " + subencoding);
1500 return false;
1501 }
1502
1503 var subrects = 0;
1504 var curr_tile = this._FBU.total_tiles - this._FBU.tiles;
1505 var tile_x = curr_tile % this._FBU.tiles_x;
1506 var tile_y = Math.floor(curr_tile / this._FBU.tiles_x);
1507 var x = this._FBU.x + tile_x * 16;
1508 var y = this._FBU.y + tile_y * 16;
1509 var w = Math.min(16, (this._FBU.x + this._FBU.width) - x);
1510 var h = Math.min(16, (this._FBU.y + this._FBU.height) - y);
1511
1512 // Figure out how much we are expecting
1513 if (subencoding & 0x01) { // Raw
1514 this._FBU.bytes += w * h * this._fb_Bpp;
1515 } else {
1516 if (subencoding & 0x02) { // Background
1517 this._FBU.bytes += this._fb_Bpp;
1518 }
1519 if (subencoding & 0x04) { // Foreground
1520 this._FBU.bytes += this._fb_Bpp;
1521 }
1522 if (subencoding & 0x08) { // AnySubrects
1523 this._FBU.bytes++; // Since we aren't shifting it off
1524 if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; }
1525 subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek
1526 if (subencoding & 0x10) { // SubrectsColoured
1527 this._FBU.bytes += subrects * (this._fb_Bpp + 2);
1528 } else {
1529 this._FBU.bytes += subrects * 2;
1530 }
1531 }
1532 }
1533
1534 if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; }
1535
1536 // We know the encoding and have a whole tile
1537 this._FBU.subencoding = rQ[rQi];
1538 rQi++;
1539 if (this._FBU.subencoding === 0) {
1540 if (this._FBU.lastsubencoding & 0x01) {
1541 // Weird: ignore blanks are RAW
1542 Util.Debug(" Ignoring blank after RAW");
1543 } else {
1544 this._display.fillRect(x, y, w, h, this._FBU.background);
1545 }
1546 } else if (this._FBU.subencoding & 0x01) { // Raw
1547 this._display.blitImage(x, y, w, h, rQ, rQi);
1548 rQi += this._FBU.bytes - 1;
1549 } else {
1550 if (this._FBU.subencoding & 0x02) { // Background
1551 this._FBU.background = rQ.slice(rQi, rQi + this._fb_Bpp);
1552 rQi += this._fb_Bpp;
1553 }
1554 if (this._FBU.subencoding & 0x04) { // Foreground
1555 this._FBU.foreground = rQ.slice(rQi, rQi + this._fb_Bpp);
1556 rQi += this._fb_Bpp;
1557 }
1558
1559 this._display.startTile(x, y, w, h, this._FBU.background);
1560 if (this._FBU.subencoding & 0x08) { // AnySubrects
1561 subrects = rQ[rQi];
1562 rQi++;
1563
1564 for (var s = 0; s < subrects; s++) {
1565 var color;
1566 if (this._FBU.subencoding & 0x10) { // SubrectsColoured
1567 color = rQ.slice(rQi, rQi + this._fb_Bpp);
1568 rQi += this._fb_Bpp;
1569 } else {
1570 color = this._FBU.foreground;
1571 }
1572 var xy = rQ[rQi];
1573 rQi++;
1574 var sx = (xy >> 4);
1575 var sy = (xy & 0x0f);
1576
1577 var wh = rQ[rQi];
1578 rQi++;
1579 var sw = (wh >> 4) + 1;
1580 var sh = (wh & 0x0f) + 1;
1581
1582 this._display.subTile(sx, sy, sw, sh, color);
1583 }
1584 }
1585 this._display.finishTile();
1586 }
1587 this._sock.set_rQi(rQi);
1588 this._FBU.lastsubencoding = this._FBU.subencoding;
1589 this._FBU.bytes = 0;
1590 this._FBU.tiles--;
1591 }
1592
1593 if (this._FBU.tiles === 0) {
1594 this._FBU.rects--;
1595 }
1596
1597 return true;
1598 },
1599
1600 getTightCLength: function (arr) {
1601 var header = 1, data = 0;
1602 data += arr[0] & 0x7f;
1603 if (arr[0] & 0x80) {
1604 header++;
1605 data += (arr[1] & 0x7f) << 7;
1606 if (arr[1] & 0x80) {
1607 header++;
1608 data += arr[2] << 14;
1609 }
1610 }
1611 return [header, data];
1612 },
1613
1614 display_tight: function (isTightPNG) {
1615 if (this._fb_depth === 1) {
1616 this._fail("Tight protocol handler only implements true color mode");
1617 }
1618
1619 this._FBU.bytes = 1; // compression-control byte
1620 if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; }
1621
1622 var checksum = function (data) {
1623 var sum = 0;
1624 for (var i = 0; i < data.length; i++) {
1625 sum += data[i];
1626 if (sum > 65536) sum -= 65536;
1627 }
1628 return sum;
1629 };
1630
1631 var resetStreams = 0;
1632 var streamId = -1;
1633 var decompress = function (data) {
1634 for (var i = 0; i < 4; i++) {
1635 if ((resetStreams >> i) & 1) {
1636 this._FBU.zlibs[i].reset();
1637 Util.Info("Reset zlib stream " + i);
1638 }
1639 }
1640
1641 var uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0);
1642 if (uncompressed.status !== 0) {
1643 Util.Error("Invalid data in zlib stream");
1644 }
1645
1646 return uncompressed.data;
1647 }.bind(this);
1648
1649 var indexedToRGB = function (data, numColors, palette, width, height) {
1650 // Convert indexed (palette based) image data to RGB
1651 // TODO: reduce number of calculations inside loop
1652 var dest = [];
1653 var x, y, dp, sp;
1654 if (numColors === 2) {
1655 var w = Math.floor((width + 7) / 8);
1656 var w1 = Math.floor(width / 8);
1657
1658 for (y = 0; y < height; y++) {
1659 var b;
1660 for (x = 0; x < w1; x++) {
1661 for (b = 7; b >= 0; b--) {
1662 dp = (y * width + x * 8 + 7 - b) * 3;
1663 sp = (data[y * w + x] >> b & 1) * 3;
1664 dest[dp] = palette[sp];
1665 dest[dp + 1] = palette[sp + 1];
1666 dest[dp + 2] = palette[sp + 2];
1667 }
1668 }
1669
1670 for (b = 7; b >= 8 - width % 8; b--) {
1671 dp = (y * width + x * 8 + 7 - b) * 3;
1672 sp = (data[y * w + x] >> b & 1) * 3;
1673 dest[dp] = palette[sp];
1674 dest[dp + 1] = palette[sp + 1];
1675 dest[dp + 2] = palette[sp + 2];
1676 }
1677 }
1678 } else {
1679 for (y = 0; y < height; y++) {
1680 for (x = 0; x < width; x++) {
1681 dp = (y * width + x) * 3;
1682 sp = data[y * width + x] * 3;
1683 dest[dp] = palette[sp];
1684 dest[dp + 1] = palette[sp + 1];
1685 dest[dp + 2] = palette[sp + 2];
1686 }
1687 }
1688 }
1689
1690 return dest;
1691 }.bind(this);
1692
1693 var rQ = this._sock.get_rQ();
1694 var rQi = this._sock.get_rQi();
1695 var cmode, clength, data;
1696
1697 var handlePalette = function () {
1698 var numColors = rQ[rQi + 2] + 1;
1699 var paletteSize = numColors * this._fb_depth;
1700 this._FBU.bytes += paletteSize;
1701 if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; }
1702
1703 var bpp = (numColors <= 2) ? 1 : 8;
1704 var rowSize = Math.floor((this._FBU.width * bpp + 7) / 8);
1705 var raw = false;
1706 if (rowSize * this._FBU.height < 12) {
1707 raw = true;
1708 clength = [0, rowSize * this._FBU.height];
1709 } else {
1710 clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(3 + paletteSize,
1711 3 + paletteSize + 3));
1712 }
1713
1714 this._FBU.bytes += clength[0] + clength[1];
1715 if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
1716
1717 // Shift ctl, filter id, num colors, palette entries, and clength off
1718 this._sock.rQskipBytes(3);
1719 var palette = this._sock.rQshiftBytes(paletteSize);
1720 this._sock.rQskipBytes(clength[0]);
1721
1722 if (raw) {
1723 data = this._sock.rQshiftBytes(clength[1]);
1724 } else {
1725 data = decompress(this._sock.rQshiftBytes(clength[1]));
1726 }
1727
1728 // Convert indexed (palette based) image data to RGB
1729 var rgb = indexedToRGB(data, numColors, palette, this._FBU.width, this._FBU.height);
1730
1731 this._display.renderQ_push({
1732 'type': 'blitRgb',
1733 'data': rgb,
1734 'x': this._FBU.x,
1735 'y': this._FBU.y,
1736 'width': this._FBU.width,
1737 'height': this._FBU.height
1738 });
1739
1740 return true;
1741 }.bind(this);
1742
1743 var handleCopy = function () {
1744 var raw = false;
1745 var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth;
1746 if (uncompressedSize < 12) {
1747 raw = true;
1748 clength = [0, uncompressedSize];
1749 } else {
1750 clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4));
1751 }
1752 this._FBU.bytes = 1 + clength[0] + clength[1];
1753 if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
1754
1755 // Shift ctl, clength off
1756 this._sock.rQshiftBytes(1 + clength[0]);
1757
1758 if (raw) {
1759 data = this._sock.rQshiftBytes(clength[1]);
1760 } else {
1761 data = decompress(this._sock.rQshiftBytes(clength[1]));
1762 }
1763
1764 this._display.renderQ_push({
1765 'type': 'blitRgb',
1766 'data': data,
1767 'x': this._FBU.x,
1768 'y': this._FBU.y,
1769 'width': this._FBU.width,
1770 'height': this._FBU.height
1771 });
1772
1773 return true;
1774 }.bind(this);
1775
1776 var ctl = this._sock.rQpeek8();
1777
1778 // Keep tight reset bits
1779 resetStreams = ctl & 0xF;
1780
1781 // Figure out filter
1782 ctl = ctl >> 4;
1783 streamId = ctl & 0x3;
1784
1785 if (ctl === 0x08) cmode = "fill";
1786 else if (ctl === 0x09) cmode = "jpeg";
1787 else if (ctl === 0x0A) cmode = "png";
1788 else if (ctl & 0x04) cmode = "filter";
1789 else if (ctl < 0x04) cmode = "copy";
1790 else return this._fail("Illegal tight compression received, ctl: " + ctl);
1791
1792 if (isTightPNG && (cmode === "filter" || cmode === "copy")) {
1793 return this._fail("filter/copy received in tightPNG mode");
1794 }
1795
1796 switch (cmode) {
1797 // fill use fb_depth because TPIXELs drop the padding byte
1798 case "fill": // TPIXEL
1799 this._FBU.bytes += this._fb_depth;
1800 break;
1801 case "jpeg": // max clength
1802 this._FBU.bytes += 3;
1803 break;
1804 case "png": // max clength
1805 this._FBU.bytes += 3;
1806 break;
1807 case "filter": // filter id + num colors if palette
1808 this._FBU.bytes += 2;
1809 break;
1810 case "copy":
1811 break;
1812 }
1813
1814 if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
1815
1816 // Determine FBU.bytes
1817 switch (cmode) {
1818 case "fill":
1819 this._sock.rQskip8(); // shift off ctl
1820 var color = this._sock.rQshiftBytes(this._fb_depth);
1821 this._display.renderQ_push({
1822 'type': 'fill',
1823 'x': this._FBU.x,
1824 'y': this._FBU.y,
1825 'width': this._FBU.width,
1826 'height': this._FBU.height,
1827 'color': [color[2], color[1], color[0]]
1828 });
1829 break;
1830 case "png":
1831 case "jpeg":
1832 clength = RFB.encodingHandlers.getTightCLength(this._sock.rQslice(1, 4));
1833 this._FBU.bytes = 1 + clength[0] + clength[1]; // ctl + clength size + jpeg-data
1834 if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; }
1835
1836 // We have everything, render it
1837 this._sock.rQskipBytes(1 + clength[0]); // shift off clt + compact length
1838 var img = new Image();
1839 img.src = "data: image/" + cmode +
1840 RFB.extract_data_uri(this._sock.rQshiftBytes(clength[1]));
1841 this._display.renderQ_push({
1842 'type': 'img',
1843 'img': img,
1844 'x': this._FBU.x,
1845 'y': this._FBU.y
1846 });
1847 img = null;
1848 break;
1849 case "filter":
1850 var filterId = rQ[rQi + 1];
1851 if (filterId === 1) {
1852 if (!handlePalette()) { return false; }
1853 } else {
1854 // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter
1855 // Filter 2, Gradient is valid but not use if jpeg is enabled
1856 // TODO(directxman12): why aren't we just calling '_fail' here
1857 throw new Error("Unsupported tight subencoding received, filter: " + filterId);
1858 }
1859 break;
1860 case "copy":
1861 if (!handleCopy()) { return false; }
1862 break;
1863 }
1864
1865
1866 this._FBU.bytes = 0;
1867 this._FBU.rects--;
1868
1869 return true;
1870 },
1871
1872 TIGHT: function () { return this._encHandlers.display_tight(false); },
1873 TIGHT_PNG: function () { return this._encHandlers.display_tight(true); },
1874
1875 last_rect: function () {
1876 this._FBU.rects = 0;
1877 return true;
1878 },
1879
1880 handle_FB_resize: function () {
1881 this._fb_width = this._FBU.width;
1882 this._fb_height = this._FBU.height;
1883 this._display.resize(this._fb_width, this._fb_height);
1884 this._onFBResize(this, this._fb_width, this._fb_height);
1885 this._timing.fbu_rt_start = (new Date()).getTime();
1886
1887 this._FBU.bytes = 0;
1888 this._FBU.rects -= 1;
1889 return true;
1890 },
1891
1892 ExtendedDesktopSize: function () {
1893 this._FBU.bytes = 1;
1894 if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; }
1895
1896 this._supportsSetDesktopSize = true;
1897 var number_of_screens = this._sock.rQpeek8();
1898
1899 this._FBU.bytes = 4 + (number_of_screens * 16);
1900 if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; }
1901
1902 this._sock.rQskipBytes(1); // number-of-screens
1903 this._sock.rQskipBytes(3); // padding
1904
1905 for (var i=0; i<number_of_screens; i += 1) {
1906 // Save the id and flags of the first screen
1907 if (i == 0) {
1908 this._screen_id = this._sock.rQshiftBytes(4); // id
1909 this._sock.rQskipBytes(2); // x-position
1910 this._sock.rQskipBytes(2); // y-position
1911 this._sock.rQskipBytes(2); // width
1912 this._sock.rQskipBytes(2); // height
1913 this._screen_flags = this._sock.rQshiftBytes(4); // flags
1914 } else {
1915 this._sock.rQskipBytes(16);
1916 }
1917 }
1918
1919 /*
1920 * The x-position indicates the reason for the change:
1921 *
1922 * 0 - server resized on its own
1923 * 1 - this client requested the resize
1924 * 2 - another client requested the resize
1925 */
1926
1927 // We need to handle errors when we requested the resize.
1928 if (this._FBU.x == 1 && this._FBU.y != 0) {
1929 var msg = "";
1930 // The y-position indicates the status code from the server
1931 switch (this._FBU.y) {
1932 case 1:
1933 msg = "Resize is administratively prohibited";
1934 break;
1935 case 2:
1936 msg = "Out of resources";
1937 break;
1938 case 3:
1939 msg = "Invalid screen layout";
1940 break;
1941 default:
1942 msg = "Unknown reason";
1943 break;
1944 }
1945 Util.Info("Server did not accept the resize request: " + msg);
1946 return true;
1947 }
1948
1949 this._encHandlers.handle_FB_resize();
1950 return true;
1951 },
1952
1953 DesktopSize: function () {
1954 this._encHandlers.handle_FB_resize();
1955 return true;
1956 },
1957
1958 Cursor: function () {
1959 Util.Debug(">> set_cursor");
1960 var x = this._FBU.x; // hotspot-x
1961 var y = this._FBU.y; // hotspot-y
1962 var w = this._FBU.width;
1963 var h = this._FBU.height;
1964
1965 var pixelslength = w * h * this._fb_Bpp;
1966 var masklength = Math.floor((w + 7) / 8) * h;
1967
1968 this._FBU.bytes = pixelslength + masklength;
1969 if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; }
1970
1971 this._display.changeCursor(this._sock.rQshiftBytes(pixelslength),
1972 this._sock.rQshiftBytes(masklength),
1973 x, y, w, h);
1974
1975 this._FBU.bytes = 0;
1976 this._FBU.rects--;
1977
1978 Util.Debug("<< set_cursor");
1979 return true;
1980 },
1981
1982 JPEG_quality_lo: function () {
1983 Util.Error("Server sent jpeg_quality pseudo-encoding");
1984 },
1985
1986 compress_lo: function () {
1987 Util.Error("Server sent compress level pseudo-encoding");
1988 }
1989 };
1990 })();