]> git.proxmox.com Git - mirror_novnc.git/blob - core/rfb.js
Merge branch 'npm' of https://github.com/CendioOssman/noVNC
[mirror_novnc.git] / core / rfb.js
1 /*
2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2018 The noVNC Authors
4 * Licensed under MPL 2.0 (see LICENSE.txt)
5 *
6 * See README.md for usage and integration instructions.
7 *
8 */
9
10 import * as Log from './util/logging.js';
11 import { decodeUTF8 } from './util/strings.js';
12 import { dragThreshold } from './util/browser.js';
13 import EventTargetMixin from './util/eventtarget.js';
14 import Display from "./display.js";
15 import Keyboard from "./input/keyboard.js";
16 import Mouse from "./input/mouse.js";
17 import Cursor from "./util/cursor.js";
18 import Websock from "./websock.js";
19 import DES from "./des.js";
20 import KeyTable from "./input/keysym.js";
21 import XtScancode from "./input/xtscancodes.js";
22 import { encodings } from "./encodings.js";
23 import "./util/polyfill.js";
24
25 import RawDecoder from "./decoders/raw.js";
26 import CopyRectDecoder from "./decoders/copyrect.js";
27 import RREDecoder from "./decoders/rre.js";
28 import HextileDecoder from "./decoders/hextile.js";
29 import TightDecoder from "./decoders/tight.js";
30 import TightPNGDecoder from "./decoders/tightpng.js";
31
32 // How many seconds to wait for a disconnect to finish
33 const DISCONNECT_TIMEOUT = 3;
34
35 export default class RFB extends EventTargetMixin {
36 constructor(target, url, options) {
37 if (!target) {
38 throw new Error("Must specify target");
39 }
40 if (!url) {
41 throw new Error("Must specify URL");
42 }
43
44 super();
45
46 this._target = target;
47 this._url = url;
48
49 // Connection details
50 options = options || {};
51 this._rfb_credentials = options.credentials || {};
52 this._shared = 'shared' in options ? !!options.shared : true;
53 this._repeaterID = options.repeaterID || '';
54 this._showDotCursor = options.showDotCursor || false;
55
56 // Internal state
57 this._rfb_connection_state = '';
58 this._rfb_init_state = '';
59 this._rfb_auth_scheme = '';
60 this._rfb_clean_disconnect = true;
61
62 // Server capabilities
63 this._rfb_version = 0;
64 this._rfb_max_version = 3.8;
65 this._rfb_tightvnc = false;
66 this._rfb_xvp_ver = 0;
67
68 this._fb_width = 0;
69 this._fb_height = 0;
70
71 this._fb_name = "";
72
73 this._capabilities = { power: false };
74
75 this._supportsFence = false;
76
77 this._supportsContinuousUpdates = false;
78 this._enabledContinuousUpdates = false;
79
80 this._supportsSetDesktopSize = false;
81 this._screen_id = 0;
82 this._screen_flags = 0;
83
84 this._qemuExtKeyEventSupported = false;
85
86 // Internal objects
87 this._sock = null; // Websock object
88 this._display = null; // Display object
89 this._flushing = false; // Display flushing state
90 this._keyboard = null; // Keyboard input handler object
91 this._mouse = null; // Mouse input handler object
92
93 // Timers
94 this._disconnTimer = null; // disconnection timer
95 this._resizeTimeout = null; // resize rate limiting
96
97 // Decoder states
98 this._decoders = {};
99
100 this._FBU = {
101 rects: 0,
102 x: 0,
103 y: 0,
104 width: 0,
105 height: 0,
106 encoding: null,
107 };
108
109 // Mouse state
110 this._mouse_buttonMask = 0;
111 this._mouse_arr = [];
112 this._viewportDragging = false;
113 this._viewportDragPos = {};
114 this._viewportHasMoved = false;
115
116 // Bound event handlers
117 this._eventHandlers = {
118 focusCanvas: this._focusCanvas.bind(this),
119 windowResize: this._windowResize.bind(this),
120 };
121
122 // main setup
123 Log.Debug(">> RFB.constructor");
124
125 // Create DOM elements
126 this._screen = document.createElement('div');
127 this._screen.style.display = 'flex';
128 this._screen.style.width = '100%';
129 this._screen.style.height = '100%';
130 this._screen.style.overflow = 'auto';
131 this._screen.style.backgroundColor = 'rgb(40, 40, 40)';
132 this._canvas = document.createElement('canvas');
133 this._canvas.style.margin = 'auto';
134 // Some browsers add an outline on focus
135 this._canvas.style.outline = 'none';
136 // IE miscalculates width without this :(
137 this._canvas.style.flexShrink = '0';
138 this._canvas.width = 0;
139 this._canvas.height = 0;
140 this._canvas.tabIndex = -1;
141 this._screen.appendChild(this._canvas);
142
143 // Cursor
144 this._cursor = new Cursor();
145
146 // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
147 // it. Result: no cursor at all until a window border or an edit field
148 // is hit blindly. But there are also VNC servers that draw the cursor
149 // in the framebuffer and don't send the empty local cursor. There is
150 // no way to satisfy both sides.
151 //
152 // The spec is unclear on this "initial cursor" issue. Many other
153 // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
154 // initial cursor instead.
155 this._cursorImage = RFB.cursors.none;
156
157 // populate decoder array with objects
158 this._decoders[encodings.encodingRaw] = new RawDecoder();
159 this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
160 this._decoders[encodings.encodingRRE] = new RREDecoder();
161 this._decoders[encodings.encodingHextile] = new HextileDecoder();
162 this._decoders[encodings.encodingTight] = new TightDecoder();
163 this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
164
165 // NB: nothing that needs explicit teardown should be done
166 // before this point, since this can throw an exception
167 try {
168 this._display = new Display(this._canvas);
169 } catch (exc) {
170 Log.Error("Display exception: " + exc);
171 throw exc;
172 }
173 this._display.onflush = this._onFlush.bind(this);
174 this._display.clear();
175
176 this._keyboard = new Keyboard(this._canvas);
177 this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
178
179 this._mouse = new Mouse(this._canvas);
180 this._mouse.onmousebutton = this._handleMouseButton.bind(this);
181 this._mouse.onmousemove = this._handleMouseMove.bind(this);
182
183 this._sock = new Websock();
184 this._sock.on('message', this._handle_message.bind(this));
185 this._sock.on('open', () => {
186 if ((this._rfb_connection_state === 'connecting') &&
187 (this._rfb_init_state === '')) {
188 this._rfb_init_state = 'ProtocolVersion';
189 Log.Debug("Starting VNC handshake");
190 } else {
191 this._fail("Unexpected server connection while " +
192 this._rfb_connection_state);
193 }
194 });
195 this._sock.on('close', (e) => {
196 Log.Debug("WebSocket on-close event");
197 let msg = "";
198 if (e.code) {
199 msg = "(code: " + e.code;
200 if (e.reason) {
201 msg += ", reason: " + e.reason;
202 }
203 msg += ")";
204 }
205 switch (this._rfb_connection_state) {
206 case 'connecting':
207 this._fail("Connection closed " + msg);
208 break;
209 case 'connected':
210 // Handle disconnects that were initiated server-side
211 this._updateConnectionState('disconnecting');
212 this._updateConnectionState('disconnected');
213 break;
214 case 'disconnecting':
215 // Normal disconnection path
216 this._updateConnectionState('disconnected');
217 break;
218 case 'disconnected':
219 this._fail("Unexpected server disconnect " +
220 "when already disconnected " + msg);
221 break;
222 default:
223 this._fail("Unexpected server disconnect before connecting " +
224 msg);
225 break;
226 }
227 this._sock.off('close');
228 });
229 this._sock.on('error', e => Log.Warn("WebSocket on-error event"));
230
231 // Slight delay of the actual connection so that the caller has
232 // time to set up callbacks
233 setTimeout(this._updateConnectionState.bind(this, 'connecting'));
234
235 Log.Debug("<< RFB.constructor");
236
237 // ===== PROPERTIES =====
238
239 this.dragViewport = false;
240 this.focusOnClick = true;
241
242 this._viewOnly = false;
243 this._clipViewport = false;
244 this._scaleViewport = false;
245 this._resizeSession = false;
246 }
247
248 // ===== PROPERTIES =====
249
250 get viewOnly() { return this._viewOnly; }
251 set viewOnly(viewOnly) {
252 this._viewOnly = viewOnly;
253
254 if (this._rfb_connection_state === "connecting" ||
255 this._rfb_connection_state === "connected") {
256 if (viewOnly) {
257 this._keyboard.ungrab();
258 this._mouse.ungrab();
259 } else {
260 this._keyboard.grab();
261 this._mouse.grab();
262 }
263 }
264 }
265
266 get capabilities() { return this._capabilities; }
267
268 get touchButton() { return this._mouse.touchButton; }
269 set touchButton(button) { this._mouse.touchButton = button; }
270
271 get clipViewport() { return this._clipViewport; }
272 set clipViewport(viewport) {
273 this._clipViewport = viewport;
274 this._updateClip();
275 }
276
277 get scaleViewport() { return this._scaleViewport; }
278 set scaleViewport(scale) {
279 this._scaleViewport = scale;
280 // Scaling trumps clipping, so we may need to adjust
281 // clipping when enabling or disabling scaling
282 if (scale && this._clipViewport) {
283 this._updateClip();
284 }
285 this._updateScale();
286 if (!scale && this._clipViewport) {
287 this._updateClip();
288 }
289 }
290
291 get resizeSession() { return this._resizeSession; }
292 set resizeSession(resize) {
293 this._resizeSession = resize;
294 if (resize) {
295 this._requestRemoteResize();
296 }
297 }
298
299 get showDotCursor() { return this._showDotCursor; }
300 set showDotCursor(show) {
301 this._showDotCursor = show;
302 this._refreshCursor();
303 }
304
305 // ===== PUBLIC METHODS =====
306
307 disconnect() {
308 this._updateConnectionState('disconnecting');
309 this._sock.off('error');
310 this._sock.off('message');
311 this._sock.off('open');
312 }
313
314 sendCredentials(creds) {
315 this._rfb_credentials = creds;
316 setTimeout(this._init_msg.bind(this), 0);
317 }
318
319 sendCtrlAltDel() {
320 if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
321 Log.Info("Sending Ctrl-Alt-Del");
322
323 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
324 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
325 this.sendKey(KeyTable.XK_Delete, "Delete", true);
326 this.sendKey(KeyTable.XK_Delete, "Delete", false);
327 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
328 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
329 }
330
331 machineShutdown() {
332 this._xvpOp(1, 2);
333 }
334
335 machineReboot() {
336 this._xvpOp(1, 3);
337 }
338
339 machineReset() {
340 this._xvpOp(1, 4);
341 }
342
343 // Send a key press. If 'down' is not specified then send a down key
344 // followed by an up key.
345 sendKey(keysym, code, down) {
346 if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
347
348 if (down === undefined) {
349 this.sendKey(keysym, code, true);
350 this.sendKey(keysym, code, false);
351 return;
352 }
353
354 const scancode = XtScancode[code];
355
356 if (this._qemuExtKeyEventSupported && scancode) {
357 // 0 is NoSymbol
358 keysym = keysym || 0;
359
360 Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
361
362 RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
363 } else {
364 if (!keysym) {
365 return;
366 }
367 Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
368 RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
369 }
370 }
371
372 focus() {
373 this._canvas.focus();
374 }
375
376 blur() {
377 this._canvas.blur();
378 }
379
380 clipboardPasteFrom(text) {
381 if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
382 RFB.messages.clientCutText(this._sock, text);
383 }
384
385 // ===== PRIVATE METHODS =====
386
387 _connect() {
388 Log.Debug(">> RFB.connect");
389
390 Log.Info("connecting to " + this._url);
391
392 try {
393 // WebSocket.onopen transitions to the RFB init states
394 this._sock.open(this._url, ['binary']);
395 } catch (e) {
396 if (e.name === 'SyntaxError') {
397 this._fail("Invalid host or port (" + e + ")");
398 } else {
399 this._fail("Error when opening socket (" + e + ")");
400 }
401 }
402
403 // Make our elements part of the page
404 this._target.appendChild(this._screen);
405
406 this._cursor.attach(this._canvas);
407 this._refreshCursor();
408
409 // Monitor size changes of the screen
410 // FIXME: Use ResizeObserver, or hidden overflow
411 window.addEventListener('resize', this._eventHandlers.windowResize);
412
413 // Always grab focus on some kind of click event
414 this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
415 this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);
416
417 Log.Debug("<< RFB.connect");
418 }
419
420 _disconnect() {
421 Log.Debug(">> RFB.disconnect");
422 this._cursor.detach();
423 this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
424 this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
425 window.removeEventListener('resize', this._eventHandlers.windowResize);
426 this._keyboard.ungrab();
427 this._mouse.ungrab();
428 this._sock.close();
429 try {
430 this._target.removeChild(this._screen);
431 } catch (e) {
432 if (e.name === 'NotFoundError') {
433 // Some cases where the initial connection fails
434 // can disconnect before the _screen is created
435 } else {
436 throw e;
437 }
438 }
439 clearTimeout(this._resizeTimeout);
440 Log.Debug("<< RFB.disconnect");
441 }
442
443 _focusCanvas(event) {
444 // Respect earlier handlers' request to not do side-effects
445 if (event.defaultPrevented) {
446 return;
447 }
448
449 if (!this.focusOnClick) {
450 return;
451 }
452
453 this.focus();
454 }
455
456 _windowResize(event) {
457 // If the window resized then our screen element might have
458 // as well. Update the viewport dimensions.
459 window.requestAnimationFrame(() => {
460 this._updateClip();
461 this._updateScale();
462 });
463
464 if (this._resizeSession) {
465 // Request changing the resolution of the remote display to
466 // the size of the local browser viewport.
467
468 // In order to not send multiple requests before the browser-resize
469 // is finished we wait 0.5 seconds before sending the request.
470 clearTimeout(this._resizeTimeout);
471 this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500);
472 }
473 }
474
475 // Update state of clipping in Display object, and make sure the
476 // configured viewport matches the current screen size
477 _updateClip() {
478 const cur_clip = this._display.clipViewport;
479 let new_clip = this._clipViewport;
480
481 if (this._scaleViewport) {
482 // Disable viewport clipping if we are scaling
483 new_clip = false;
484 }
485
486 if (cur_clip !== new_clip) {
487 this._display.clipViewport = new_clip;
488 }
489
490 if (new_clip) {
491 // When clipping is enabled, the screen is limited to
492 // the size of the container.
493 const size = this._screenSize();
494 this._display.viewportChangeSize(size.w, size.h);
495 this._fixScrollbars();
496 }
497 }
498
499 _updateScale() {
500 if (!this._scaleViewport) {
501 this._display.scale = 1.0;
502 } else {
503 const size = this._screenSize();
504 this._display.autoscale(size.w, size.h);
505 }
506 this._fixScrollbars();
507 }
508
509 // Requests a change of remote desktop size. This message is an extension
510 // and may only be sent if we have received an ExtendedDesktopSize message
511 _requestRemoteResize() {
512 clearTimeout(this._resizeTimeout);
513 this._resizeTimeout = null;
514
515 if (!this._resizeSession || this._viewOnly ||
516 !this._supportsSetDesktopSize) {
517 return;
518 }
519
520 const size = this._screenSize();
521 RFB.messages.setDesktopSize(this._sock,
522 Math.floor(size.w), Math.floor(size.h),
523 this._screen_id, this._screen_flags);
524
525 Log.Debug('Requested new desktop size: ' +
526 size.w + 'x' + size.h);
527 }
528
529 // Gets the the size of the available screen
530 _screenSize() {
531 let r = this._screen.getBoundingClientRect();
532 return { w: r.width, h: r.height };
533 }
534
535 _fixScrollbars() {
536 // This is a hack because Chrome screws up the calculation
537 // for when scrollbars are needed. So to fix it we temporarily
538 // toggle them off and on.
539 const orig = this._screen.style.overflow;
540 this._screen.style.overflow = 'hidden';
541 // Force Chrome to recalculate the layout by asking for
542 // an element's dimensions
543 this._screen.getBoundingClientRect();
544 this._screen.style.overflow = orig;
545 }
546
547 /*
548 * Connection states:
549 * connecting
550 * connected
551 * disconnecting
552 * disconnected - permanent state
553 */
554 _updateConnectionState(state) {
555 const oldstate = this._rfb_connection_state;
556
557 if (state === oldstate) {
558 Log.Debug("Already in state '" + state + "', ignoring");
559 return;
560 }
561
562 // The 'disconnected' state is permanent for each RFB object
563 if (oldstate === 'disconnected') {
564 Log.Error("Tried changing state of a disconnected RFB object");
565 return;
566 }
567
568 // Ensure proper transitions before doing anything
569 switch (state) {
570 case 'connected':
571 if (oldstate !== 'connecting') {
572 Log.Error("Bad transition to connected state, " +
573 "previous connection state: " + oldstate);
574 return;
575 }
576 break;
577
578 case 'disconnected':
579 if (oldstate !== 'disconnecting') {
580 Log.Error("Bad transition to disconnected state, " +
581 "previous connection state: " + oldstate);
582 return;
583 }
584 break;
585
586 case 'connecting':
587 if (oldstate !== '') {
588 Log.Error("Bad transition to connecting state, " +
589 "previous connection state: " + oldstate);
590 return;
591 }
592 break;
593
594 case 'disconnecting':
595 if (oldstate !== 'connected' && oldstate !== 'connecting') {
596 Log.Error("Bad transition to disconnecting state, " +
597 "previous connection state: " + oldstate);
598 return;
599 }
600 break;
601
602 default:
603 Log.Error("Unknown connection state: " + state);
604 return;
605 }
606
607 // State change actions
608
609 this._rfb_connection_state = state;
610
611 Log.Debug("New state '" + state + "', was '" + oldstate + "'.");
612
613 if (this._disconnTimer && state !== 'disconnecting') {
614 Log.Debug("Clearing disconnect timer");
615 clearTimeout(this._disconnTimer);
616 this._disconnTimer = null;
617
618 // make sure we don't get a double event
619 this._sock.off('close');
620 }
621
622 switch (state) {
623 case 'connecting':
624 this._connect();
625 break;
626
627 case 'connected':
628 this.dispatchEvent(new CustomEvent("connect", { detail: {} }));
629 break;
630
631 case 'disconnecting':
632 this._disconnect();
633
634 this._disconnTimer = setTimeout(() => {
635 Log.Error("Disconnection timed out.");
636 this._updateConnectionState('disconnected');
637 }, DISCONNECT_TIMEOUT * 1000);
638 break;
639
640 case 'disconnected':
641 this.dispatchEvent(new CustomEvent(
642 "disconnect", { detail:
643 { clean: this._rfb_clean_disconnect } }));
644 break;
645 }
646 }
647
648 /* Print errors and disconnect
649 *
650 * The parameter 'details' is used for information that
651 * should be logged but not sent to the user interface.
652 */
653 _fail(details) {
654 switch (this._rfb_connection_state) {
655 case 'disconnecting':
656 Log.Error("Failed when disconnecting: " + details);
657 break;
658 case 'connected':
659 Log.Error("Failed while connected: " + details);
660 break;
661 case 'connecting':
662 Log.Error("Failed when connecting: " + details);
663 break;
664 default:
665 Log.Error("RFB failure: " + details);
666 break;
667 }
668 this._rfb_clean_disconnect = false; //This is sent to the UI
669
670 // Transition to disconnected without waiting for socket to close
671 this._updateConnectionState('disconnecting');
672 this._updateConnectionState('disconnected');
673
674 return false;
675 }
676
677 _setCapability(cap, val) {
678 this._capabilities[cap] = val;
679 this.dispatchEvent(new CustomEvent("capabilities",
680 { detail: { capabilities: this._capabilities } }));
681 }
682
683 _handle_message() {
684 if (this._sock.rQlen === 0) {
685 Log.Warn("handle_message called on an empty receive queue");
686 return;
687 }
688
689 switch (this._rfb_connection_state) {
690 case 'disconnected':
691 Log.Error("Got data while disconnected");
692 break;
693 case 'connected':
694 while (true) {
695 if (this._flushing) {
696 break;
697 }
698 if (!this._normal_msg()) {
699 break;
700 }
701 if (this._sock.rQlen === 0) {
702 break;
703 }
704 }
705 break;
706 default:
707 this._init_msg();
708 break;
709 }
710 }
711
712 _handleKeyEvent(keysym, code, down) {
713 this.sendKey(keysym, code, down);
714 }
715
716 _handleMouseButton(x, y, down, bmask) {
717 if (down) {
718 this._mouse_buttonMask |= bmask;
719 } else {
720 this._mouse_buttonMask &= ~bmask;
721 }
722
723 if (this.dragViewport) {
724 if (down && !this._viewportDragging) {
725 this._viewportDragging = true;
726 this._viewportDragPos = {'x': x, 'y': y};
727 this._viewportHasMoved = false;
728
729 // Skip sending mouse events
730 return;
731 } else {
732 this._viewportDragging = false;
733
734 // If we actually performed a drag then we are done
735 // here and should not send any mouse events
736 if (this._viewportHasMoved) {
737 return;
738 }
739
740 // Otherwise we treat this as a mouse click event.
741 // Send the button down event here, as the button up
742 // event is sent at the end of this function.
743 RFB.messages.pointerEvent(this._sock,
744 this._display.absX(x),
745 this._display.absY(y),
746 bmask);
747 }
748 }
749
750 if (this._viewOnly) { return; } // View only, skip mouse events
751
752 if (this._rfb_connection_state !== 'connected') { return; }
753 RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
754 }
755
756 _handleMouseMove(x, y) {
757 if (this._viewportDragging) {
758 const deltaX = this._viewportDragPos.x - x;
759 const deltaY = this._viewportDragPos.y - y;
760
761 if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
762 Math.abs(deltaY) > dragThreshold)) {
763 this._viewportHasMoved = true;
764
765 this._viewportDragPos = {'x': x, 'y': y};
766 this._display.viewportChangePos(deltaX, deltaY);
767 }
768
769 // Skip sending mouse events
770 return;
771 }
772
773 if (this._viewOnly) { return; } // View only, skip mouse events
774
775 if (this._rfb_connection_state !== 'connected') { return; }
776 RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
777 }
778
779 // Message Handlers
780
781 _negotiate_protocol_version() {
782 if (this._sock.rQlen < 12) {
783 return this._fail("Received incomplete protocol version.");
784 }
785
786 const sversion = this._sock.rQshiftStr(12).substr(4, 7);
787 Log.Info("Server ProtocolVersion: " + sversion);
788 let is_repeater = 0;
789 switch (sversion) {
790 case "000.000": // UltraVNC repeater
791 is_repeater = 1;
792 break;
793 case "003.003":
794 case "003.006": // UltraVNC
795 case "003.889": // Apple Remote Desktop
796 this._rfb_version = 3.3;
797 break;
798 case "003.007":
799 this._rfb_version = 3.7;
800 break;
801 case "003.008":
802 case "004.000": // Intel AMT KVM
803 case "004.001": // RealVNC 4.6
804 case "005.000": // RealVNC 5.3
805 this._rfb_version = 3.8;
806 break;
807 default:
808 return this._fail("Invalid server version " + sversion);
809 }
810
811 if (is_repeater) {
812 let repeaterID = "ID:" + this._repeaterID;
813 while (repeaterID.length < 250) {
814 repeaterID += "\0";
815 }
816 this._sock.send_string(repeaterID);
817 return true;
818 }
819
820 if (this._rfb_version > this._rfb_max_version) {
821 this._rfb_version = this._rfb_max_version;
822 }
823
824 const cversion = "00" + parseInt(this._rfb_version, 10) +
825 ".00" + ((this._rfb_version * 10) % 10);
826 this._sock.send_string("RFB " + cversion + "\n");
827 Log.Debug('Sent ProtocolVersion: ' + cversion);
828
829 this._rfb_init_state = 'Security';
830 }
831
832 _negotiate_security() {
833 // Polyfill since IE and PhantomJS doesn't have
834 // TypedArray.includes()
835 function includes(item, array) {
836 for (let i = 0; i < array.length; i++) {
837 if (array[i] === item) {
838 return true;
839 }
840 }
841 return false;
842 }
843
844 if (this._rfb_version >= 3.7) {
845 // Server sends supported list, client decides
846 const num_types = this._sock.rQshift8();
847 if (this._sock.rQwait("security type", num_types, 1)) { return false; }
848
849 if (num_types === 0) {
850 return this._handle_security_failure("no security types");
851 }
852
853 const types = this._sock.rQshiftBytes(num_types);
854 Log.Debug("Server security types: " + types);
855
856 // Look for each auth in preferred order
857 this._rfb_auth_scheme = 0;
858 if (includes(1, types)) {
859 this._rfb_auth_scheme = 1; // None
860 } else if (includes(22, types)) {
861 this._rfb_auth_scheme = 22; // XVP
862 } else if (includes(16, types)) {
863 this._rfb_auth_scheme = 16; // Tight
864 } else if (includes(2, types)) {
865 this._rfb_auth_scheme = 2; // VNC Auth
866 } else {
867 return this._fail("Unsupported security types (types: " + types + ")");
868 }
869
870 this._sock.send([this._rfb_auth_scheme]);
871 } else {
872 // Server decides
873 if (this._sock.rQwait("security scheme", 4)) { return false; }
874 this._rfb_auth_scheme = this._sock.rQshift32();
875 }
876
877 this._rfb_init_state = 'Authentication';
878 Log.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme);
879
880 return this._init_msg(); // jump to authentication
881 }
882
883 /*
884 * Get the security failure reason if sent from the server and
885 * send the 'securityfailure' event.
886 *
887 * - The optional parameter context can be used to add some extra
888 * context to the log output.
889 *
890 * - The optional parameter security_result_status can be used to
891 * add a custom status code to the event.
892 */
893 _handle_security_failure(context, security_result_status) {
894
895 if (typeof context === 'undefined') {
896 context = "";
897 } else {
898 context = " on " + context;
899 }
900
901 if (typeof security_result_status === 'undefined') {
902 security_result_status = 1; // fail
903 }
904
905 if (this._sock.rQwait("reason length", 4)) {
906 return false;
907 }
908 const strlen = this._sock.rQshift32();
909 let reason = "";
910
911 if (strlen > 0) {
912 if (this._sock.rQwait("reason", strlen, 8)) { return false; }
913 reason = this._sock.rQshiftStr(strlen);
914 }
915
916 if (reason !== "") {
917 this.dispatchEvent(new CustomEvent(
918 "securityfailure",
919 { detail: { status: security_result_status, reason: reason } }));
920
921 return this._fail("Security negotiation failed" + context +
922 " (reason: " + reason + ")");
923 } else {
924 this.dispatchEvent(new CustomEvent(
925 "securityfailure",
926 { detail: { status: security_result_status } }));
927
928 return this._fail("Security negotiation failed" + context);
929 }
930 }
931
932 // authentication
933 _negotiate_xvp_auth() {
934 if (!this._rfb_credentials.username ||
935 !this._rfb_credentials.password ||
936 !this._rfb_credentials.target) {
937 this.dispatchEvent(new CustomEvent(
938 "credentialsrequired",
939 { detail: { types: ["username", "password", "target"] } }));
940 return false;
941 }
942
943 const xvp_auth_str = String.fromCharCode(this._rfb_credentials.username.length) +
944 String.fromCharCode(this._rfb_credentials.target.length) +
945 this._rfb_credentials.username +
946 this._rfb_credentials.target;
947 this._sock.send_string(xvp_auth_str);
948 this._rfb_auth_scheme = 2;
949 return this._negotiate_authentication();
950 }
951
952 _negotiate_std_vnc_auth() {
953 if (this._sock.rQwait("auth challenge", 16)) { return false; }
954
955 if (!this._rfb_credentials.password) {
956 this.dispatchEvent(new CustomEvent(
957 "credentialsrequired",
958 { detail: { types: ["password"] } }));
959 return false;
960 }
961
962 // TODO(directxman12): make genDES not require an Array
963 const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
964 const response = RFB.genDES(this._rfb_credentials.password, challenge);
965 this._sock.send(response);
966 this._rfb_init_state = "SecurityResult";
967 return true;
968 }
969
970 _negotiate_tight_tunnels(numTunnels) {
971 const clientSupportedTunnelTypes = {
972 0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
973 };
974 const serverSupportedTunnelTypes = {};
975 // receive tunnel capabilities
976 for (let i = 0; i < numTunnels; i++) {
977 const cap_code = this._sock.rQshift32();
978 const cap_vendor = this._sock.rQshiftStr(4);
979 const cap_signature = this._sock.rQshiftStr(8);
980 serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature };
981 }
982
983 Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes);
984
985 // Siemens touch panels have a VNC server that supports NOTUNNEL,
986 // but forgets to advertise it. Try to detect such servers by
987 // looking for their custom tunnel type.
988 if (serverSupportedTunnelTypes[1] &&
989 (serverSupportedTunnelTypes[1].vendor === "SICR") &&
990 (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) {
991 Log.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
992 serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };
993 }
994
995 // choose the notunnel type
996 if (serverSupportedTunnelTypes[0]) {
997 if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
998 serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
999 return this._fail("Client's tunnel type had the incorrect " +
1000 "vendor or signature");
1001 }
1002 Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
1003 this._sock.send([0, 0, 0, 0]); // use NOTUNNEL
1004 return false; // wait until we receive the sub auth count to continue
1005 } else {
1006 return this._fail("Server wanted tunnels, but doesn't support " +
1007 "the notunnel type");
1008 }
1009 }
1010
1011 _negotiate_tight_auth() {
1012 if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation
1013 if (this._sock.rQwait("num tunnels", 4)) { return false; }
1014 const numTunnels = this._sock.rQshift32();
1015 if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
1016
1017 this._rfb_tightvnc = true;
1018
1019 if (numTunnels > 0) {
1020 this._negotiate_tight_tunnels(numTunnels);
1021 return false; // wait until we receive the sub auth to continue
1022 }
1023 }
1024
1025 // second pass, do the sub-auth negotiation
1026 if (this._sock.rQwait("sub auth count", 4)) { return false; }
1027 const subAuthCount = this._sock.rQshift32();
1028 if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected
1029 this._rfb_init_state = 'SecurityResult';
1030 return true;
1031 }
1032
1033 if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
1034
1035 const clientSupportedTypes = {
1036 'STDVNOAUTH__': 1,
1037 'STDVVNCAUTH_': 2
1038 };
1039
1040 const serverSupportedTypes = [];
1041
1042 for (let i = 0; i < subAuthCount; i++) {
1043 this._sock.rQshift32(); // capNum
1044 const capabilities = this._sock.rQshiftStr(12);
1045 serverSupportedTypes.push(capabilities);
1046 }
1047
1048 Log.Debug("Server Tight authentication types: " + serverSupportedTypes);
1049
1050 for (let authType in clientSupportedTypes) {
1051 if (serverSupportedTypes.indexOf(authType) != -1) {
1052 this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
1053 Log.Debug("Selected authentication type: " + authType);
1054
1055 switch (authType) {
1056 case 'STDVNOAUTH__': // no auth
1057 this._rfb_init_state = 'SecurityResult';
1058 return true;
1059 case 'STDVVNCAUTH_': // VNC auth
1060 this._rfb_auth_scheme = 2;
1061 return this._init_msg();
1062 default:
1063 return this._fail("Unsupported tiny auth scheme " +
1064 "(scheme: " + authType + ")");
1065 }
1066 }
1067 }
1068
1069 return this._fail("No supported sub-auth types!");
1070 }
1071
1072 _negotiate_authentication() {
1073 switch (this._rfb_auth_scheme) {
1074 case 0: // connection failed
1075 return this._handle_security_failure("authentication scheme");
1076
1077 case 1: // no auth
1078 if (this._rfb_version >= 3.8) {
1079 this._rfb_init_state = 'SecurityResult';
1080 return true;
1081 }
1082 this._rfb_init_state = 'ClientInitialisation';
1083 return this._init_msg();
1084
1085 case 22: // XVP auth
1086 return this._negotiate_xvp_auth();
1087
1088 case 2: // VNC authentication
1089 return this._negotiate_std_vnc_auth();
1090
1091 case 16: // TightVNC Security Type
1092 return this._negotiate_tight_auth();
1093
1094 default:
1095 return this._fail("Unsupported auth scheme (scheme: " +
1096 this._rfb_auth_scheme + ")");
1097 }
1098 }
1099
1100 _handle_security_result() {
1101 if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
1102
1103 const status = this._sock.rQshift32();
1104
1105 if (status === 0) { // OK
1106 this._rfb_init_state = 'ClientInitialisation';
1107 Log.Debug('Authentication OK');
1108 return this._init_msg();
1109 } else {
1110 if (this._rfb_version >= 3.8) {
1111 return this._handle_security_failure("security result", status);
1112 } else {
1113 this.dispatchEvent(new CustomEvent(
1114 "securityfailure",
1115 { detail: { status: status } }));
1116
1117 return this._fail("Security handshake failed");
1118 }
1119 }
1120 }
1121
1122 _negotiate_server_init() {
1123 if (this._sock.rQwait("server initialization", 24)) { return false; }
1124
1125 /* Screen size */
1126 const width = this._sock.rQshift16();
1127 const height = this._sock.rQshift16();
1128
1129 /* PIXEL_FORMAT */
1130 const bpp = this._sock.rQshift8();
1131 const depth = this._sock.rQshift8();
1132 const big_endian = this._sock.rQshift8();
1133 const true_color = this._sock.rQshift8();
1134
1135 const red_max = this._sock.rQshift16();
1136 const green_max = this._sock.rQshift16();
1137 const blue_max = this._sock.rQshift16();
1138 const red_shift = this._sock.rQshift8();
1139 const green_shift = this._sock.rQshift8();
1140 const blue_shift = this._sock.rQshift8();
1141 this._sock.rQskipBytes(3); // padding
1142
1143 // NB(directxman12): we don't want to call any callbacks or print messages until
1144 // *after* we're past the point where we could backtrack
1145
1146 /* Connection name/title */
1147 const name_length = this._sock.rQshift32();
1148 if (this._sock.rQwait('server init name', name_length, 24)) { return false; }
1149 this._fb_name = decodeUTF8(this._sock.rQshiftStr(name_length));
1150
1151 if (this._rfb_tightvnc) {
1152 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; }
1153 // In TightVNC mode, ServerInit message is extended
1154 const numServerMessages = this._sock.rQshift16();
1155 const numClientMessages = this._sock.rQshift16();
1156 const numEncodings = this._sock.rQshift16();
1157 this._sock.rQskipBytes(2); // padding
1158
1159 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
1160 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; }
1161
1162 // we don't actually do anything with the capability information that TIGHT sends,
1163 // so we just skip the all of this.
1164
1165 // TIGHT server message capabilities
1166 this._sock.rQskipBytes(16 * numServerMessages);
1167
1168 // TIGHT client message capabilities
1169 this._sock.rQskipBytes(16 * numClientMessages);
1170
1171 // TIGHT encoding capabilities
1172 this._sock.rQskipBytes(16 * numEncodings);
1173 }
1174
1175 // NB(directxman12): these are down here so that we don't run them multiple times
1176 // if we backtrack
1177 Log.Info("Screen: " + width + "x" + height +
1178 ", bpp: " + bpp + ", depth: " + depth +
1179 ", big_endian: " + big_endian +
1180 ", true_color: " + true_color +
1181 ", red_max: " + red_max +
1182 ", green_max: " + green_max +
1183 ", blue_max: " + blue_max +
1184 ", red_shift: " + red_shift +
1185 ", green_shift: " + green_shift +
1186 ", blue_shift: " + blue_shift);
1187
1188 if (big_endian !== 0) {
1189 Log.Warn("Server native endian is not little endian");
1190 }
1191
1192 if (red_shift !== 16) {
1193 Log.Warn("Server native red-shift is not 16");
1194 }
1195
1196 if (blue_shift !== 0) {
1197 Log.Warn("Server native blue-shift is not 0");
1198 }
1199
1200 // we're past the point where we could backtrack, so it's safe to call this
1201 this.dispatchEvent(new CustomEvent(
1202 "desktopname",
1203 { detail: { name: this._fb_name } }));
1204
1205 this._resize(width, height);
1206
1207 if (!this._viewOnly) { this._keyboard.grab(); }
1208 if (!this._viewOnly) { this._mouse.grab(); }
1209
1210 this._fb_depth = 24;
1211
1212 if (this._fb_name === "Intel(r) AMT KVM") {
1213 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
1214 this._fb_depth = 8;
1215 }
1216
1217 RFB.messages.pixelFormat(this._sock, this._fb_depth, true);
1218 this._sendEncodings();
1219 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height);
1220
1221 this._updateConnectionState('connected');
1222 return true;
1223 }
1224
1225 _sendEncodings() {
1226 const encs = [];
1227
1228 // In preference order
1229 encs.push(encodings.encodingCopyRect);
1230 // Only supported with full depth support
1231 if (this._fb_depth == 24) {
1232 encs.push(encodings.encodingTight);
1233 encs.push(encodings.encodingTightPNG);
1234 encs.push(encodings.encodingHextile);
1235 encs.push(encodings.encodingRRE);
1236 }
1237 encs.push(encodings.encodingRaw);
1238
1239 // Psuedo-encoding settings
1240 encs.push(encodings.pseudoEncodingQualityLevel0 + 6);
1241 encs.push(encodings.pseudoEncodingCompressLevel0 + 2);
1242
1243 encs.push(encodings.pseudoEncodingDesktopSize);
1244 encs.push(encodings.pseudoEncodingLastRect);
1245 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
1246 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
1247 encs.push(encodings.pseudoEncodingXvp);
1248 encs.push(encodings.pseudoEncodingFence);
1249 encs.push(encodings.pseudoEncodingContinuousUpdates);
1250
1251 if (this._fb_depth == 24) {
1252 encs.push(encodings.pseudoEncodingCursor);
1253 }
1254
1255 RFB.messages.clientEncodings(this._sock, encs);
1256 }
1257
1258 /* RFB protocol initialization states:
1259 * ProtocolVersion
1260 * Security
1261 * Authentication
1262 * SecurityResult
1263 * ClientInitialization - not triggered by server message
1264 * ServerInitialization
1265 */
1266 _init_msg() {
1267 switch (this._rfb_init_state) {
1268 case 'ProtocolVersion':
1269 return this._negotiate_protocol_version();
1270
1271 case 'Security':
1272 return this._negotiate_security();
1273
1274 case 'Authentication':
1275 return this._negotiate_authentication();
1276
1277 case 'SecurityResult':
1278 return this._handle_security_result();
1279
1280 case 'ClientInitialisation':
1281 this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
1282 this._rfb_init_state = 'ServerInitialisation';
1283 return true;
1284
1285 case 'ServerInitialisation':
1286 return this._negotiate_server_init();
1287
1288 default:
1289 return this._fail("Unknown init state (state: " +
1290 this._rfb_init_state + ")");
1291 }
1292 }
1293
1294 _handle_set_colour_map_msg() {
1295 Log.Debug("SetColorMapEntries");
1296
1297 return this._fail("Unexpected SetColorMapEntries message");
1298 }
1299
1300 _handle_server_cut_text() {
1301 Log.Debug("ServerCutText");
1302
1303 if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
1304 this._sock.rQskipBytes(3); // Padding
1305 const length = this._sock.rQshift32();
1306 if (this._sock.rQwait("ServerCutText", length, 8)) { return false; }
1307
1308 const text = this._sock.rQshiftStr(length);
1309
1310 if (this._viewOnly) { return true; }
1311
1312 this.dispatchEvent(new CustomEvent(
1313 "clipboard",
1314 { detail: { text: text } }));
1315
1316 return true;
1317 }
1318
1319 _handle_server_fence_msg() {
1320 if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; }
1321 this._sock.rQskipBytes(3); // Padding
1322 let flags = this._sock.rQshift32();
1323 let length = this._sock.rQshift8();
1324
1325 if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; }
1326
1327 if (length > 64) {
1328 Log.Warn("Bad payload length (" + length + ") in fence response");
1329 length = 64;
1330 }
1331
1332 const payload = this._sock.rQshiftStr(length);
1333
1334 this._supportsFence = true;
1335
1336 /*
1337 * Fence flags
1338 *
1339 * (1<<0) - BlockBefore
1340 * (1<<1) - BlockAfter
1341 * (1<<2) - SyncNext
1342 * (1<<31) - Request
1343 */
1344
1345 if (!(flags & (1<<31))) {
1346 return this._fail("Unexpected fence response");
1347 }
1348
1349 // Filter out unsupported flags
1350 // FIXME: support syncNext
1351 flags &= (1<<0) | (1<<1);
1352
1353 // BlockBefore and BlockAfter are automatically handled by
1354 // the fact that we process each incoming message
1355 // synchronuosly.
1356 RFB.messages.clientFence(this._sock, flags, payload);
1357
1358 return true;
1359 }
1360
1361 _handle_xvp_msg() {
1362 if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
1363 this._sock.rQskipBytes(1); // Padding
1364 const xvp_ver = this._sock.rQshift8();
1365 const xvp_msg = this._sock.rQshift8();
1366
1367 switch (xvp_msg) {
1368 case 0: // XVP_FAIL
1369 Log.Error("XVP Operation Failed");
1370 break;
1371 case 1: // XVP_INIT
1372 this._rfb_xvp_ver = xvp_ver;
1373 Log.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")");
1374 this._setCapability("power", true);
1375 break;
1376 default:
1377 this._fail("Illegal server XVP message (msg: " + xvp_msg + ")");
1378 break;
1379 }
1380
1381 return true;
1382 }
1383
1384 _normal_msg() {
1385 let msg_type;
1386 if (this._FBU.rects > 0) {
1387 msg_type = 0;
1388 } else {
1389 msg_type = this._sock.rQshift8();
1390 }
1391
1392 let first, ret;
1393 switch (msg_type) {
1394 case 0: // FramebufferUpdate
1395 ret = this._framebufferUpdate();
1396 if (ret && !this._enabledContinuousUpdates) {
1397 RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,
1398 this._fb_width, this._fb_height);
1399 }
1400 return ret;
1401
1402 case 1: // SetColorMapEntries
1403 return this._handle_set_colour_map_msg();
1404
1405 case 2: // Bell
1406 Log.Debug("Bell");
1407 this.dispatchEvent(new CustomEvent(
1408 "bell",
1409 { detail: {} }));
1410 return true;
1411
1412 case 3: // ServerCutText
1413 return this._handle_server_cut_text();
1414
1415 case 150: // EndOfContinuousUpdates
1416 first = !this._supportsContinuousUpdates;
1417 this._supportsContinuousUpdates = true;
1418 this._enabledContinuousUpdates = false;
1419 if (first) {
1420 this._enabledContinuousUpdates = true;
1421 this._updateContinuousUpdates();
1422 Log.Info("Enabling continuous updates.");
1423 } else {
1424 // FIXME: We need to send a framebufferupdaterequest here
1425 // if we add support for turning off continuous updates
1426 }
1427 return true;
1428
1429 case 248: // ServerFence
1430 return this._handle_server_fence_msg();
1431
1432 case 250: // XVP
1433 return this._handle_xvp_msg();
1434
1435 default:
1436 this._fail("Unexpected server message (type " + msg_type + ")");
1437 Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
1438 return true;
1439 }
1440 }
1441
1442 _onFlush() {
1443 this._flushing = false;
1444 // Resume processing
1445 if (this._sock.rQlen > 0) {
1446 this._handle_message();
1447 }
1448 }
1449
1450 _framebufferUpdate() {
1451 if (this._FBU.rects === 0) {
1452 if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
1453 this._sock.rQskipBytes(1); // Padding
1454 this._FBU.rects = this._sock.rQshift16();
1455
1456 // Make sure the previous frame is fully rendered first
1457 // to avoid building up an excessive queue
1458 if (this._display.pending()) {
1459 this._flushing = true;
1460 this._display.flush();
1461 return false;
1462 }
1463 }
1464
1465 while (this._FBU.rects > 0) {
1466 if (this._FBU.encoding === null) {
1467 if (this._sock.rQwait("rect header", 12)) { return false; }
1468 /* New FramebufferUpdate */
1469
1470 const hdr = this._sock.rQshiftBytes(12);
1471 this._FBU.x = (hdr[0] << 8) + hdr[1];
1472 this._FBU.y = (hdr[2] << 8) + hdr[3];
1473 this._FBU.width = (hdr[4] << 8) + hdr[5];
1474 this._FBU.height = (hdr[6] << 8) + hdr[7];
1475 this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
1476 (hdr[10] << 8) + hdr[11], 10);
1477 }
1478
1479 if (!this._handleRect()) {
1480 return false;
1481 }
1482
1483 this._FBU.rects--;
1484 this._FBU.encoding = null;
1485 }
1486
1487 this._display.flip();
1488
1489 return true; // We finished this FBU
1490 }
1491
1492 _handleRect() {
1493 switch (this._FBU.encoding) {
1494 case encodings.pseudoEncodingLastRect:
1495 this._FBU.rects = 1; // Will be decreased when we return
1496 return true;
1497
1498 case encodings.pseudoEncodingCursor:
1499 return this._handleCursor();
1500
1501 case encodings.pseudoEncodingQEMUExtendedKeyEvent:
1502 // Old Safari doesn't support creating keyboard events
1503 try {
1504 const keyboardEvent = document.createEvent("keyboardEvent");
1505 if (keyboardEvent.code !== undefined) {
1506 this._qemuExtKeyEventSupported = true;
1507 }
1508 } catch (err) {
1509 // Do nothing
1510 }
1511 return true;
1512
1513 case encodings.pseudoEncodingDesktopSize:
1514 this._resize(this._FBU.width, this._FBU.height);
1515 return true;
1516
1517 case encodings.pseudoEncodingExtendedDesktopSize:
1518 return this._handleExtendedDesktopSize();
1519
1520 default:
1521 return this._handleDataRect();
1522 }
1523 }
1524
1525 _handleCursor() {
1526 const hotx = this._FBU.x; // hotspot-x
1527 const hoty = this._FBU.y; // hotspot-y
1528 const w = this._FBU.width;
1529 const h = this._FBU.height;
1530
1531 const pixelslength = w * h * 4;
1532 const masklength = Math.ceil(w / 8) * h;
1533
1534 let bytes = pixelslength + masklength;
1535 if (this._sock.rQwait("cursor encoding", bytes)) {
1536 return false;
1537 }
1538
1539 // Decode from BGRX pixels + bit mask to RGBA
1540 const pixels = this._sock.rQshiftBytes(pixelslength);
1541 const mask = this._sock.rQshiftBytes(masklength);
1542 let rgba = new Uint8Array(w * h * 4);
1543
1544 let pix_idx = 0;
1545 for (let y = 0; y < h; y++) {
1546 for (let x = 0; x < w; x++) {
1547 let mask_idx = y * Math.ceil(w / 8) + Math.floor(x / 8);
1548 let alpha = (mask[mask_idx] << (x % 8)) & 0x80 ? 255 : 0;
1549 rgba[pix_idx ] = pixels[pix_idx + 2];
1550 rgba[pix_idx + 1] = pixels[pix_idx + 1];
1551 rgba[pix_idx + 2] = pixels[pix_idx];
1552 rgba[pix_idx + 3] = alpha;
1553 pix_idx += 4;
1554 }
1555 }
1556
1557 this._updateCursor(rgba, hotx, hoty, w, h);
1558
1559 return true;
1560 }
1561
1562 _handleExtendedDesktopSize() {
1563 if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
1564 return false;
1565 }
1566
1567 const number_of_screens = this._sock.rQpeek8();
1568
1569 let bytes = 4 + (number_of_screens * 16);
1570 if (this._sock.rQwait("ExtendedDesktopSize", bytes)) {
1571 return false;
1572 }
1573
1574 const firstUpdate = !this._supportsSetDesktopSize;
1575 this._supportsSetDesktopSize = true;
1576
1577 // Normally we only apply the current resize mode after a
1578 // window resize event. However there is no such trigger on the
1579 // initial connect. And we don't know if the server supports
1580 // resizing until we've gotten here.
1581 if (firstUpdate) {
1582 this._requestRemoteResize();
1583 }
1584
1585 this._sock.rQskipBytes(1); // number-of-screens
1586 this._sock.rQskipBytes(3); // padding
1587
1588 for (let i = 0; i < number_of_screens; i += 1) {
1589 // Save the id and flags of the first screen
1590 if (i === 0) {
1591 this._screen_id = this._sock.rQshiftBytes(4); // id
1592 this._sock.rQskipBytes(2); // x-position
1593 this._sock.rQskipBytes(2); // y-position
1594 this._sock.rQskipBytes(2); // width
1595 this._sock.rQskipBytes(2); // height
1596 this._screen_flags = this._sock.rQshiftBytes(4); // flags
1597 } else {
1598 this._sock.rQskipBytes(16);
1599 }
1600 }
1601
1602 /*
1603 * The x-position indicates the reason for the change:
1604 *
1605 * 0 - server resized on its own
1606 * 1 - this client requested the resize
1607 * 2 - another client requested the resize
1608 */
1609
1610 // We need to handle errors when we requested the resize.
1611 if (this._FBU.x === 1 && this._FBU.y !== 0) {
1612 let msg = "";
1613 // The y-position indicates the status code from the server
1614 switch (this._FBU.y) {
1615 case 1:
1616 msg = "Resize is administratively prohibited";
1617 break;
1618 case 2:
1619 msg = "Out of resources";
1620 break;
1621 case 3:
1622 msg = "Invalid screen layout";
1623 break;
1624 default:
1625 msg = "Unknown reason";
1626 break;
1627 }
1628 Log.Warn("Server did not accept the resize request: "
1629 + msg);
1630 } else {
1631 this._resize(this._FBU.width, this._FBU.height);
1632 }
1633
1634 return true;
1635 }
1636
1637 _handleDataRect() {
1638 let decoder = this._decoders[this._FBU.encoding];
1639 if (!decoder) {
1640 this._fail("Unsupported encoding (encoding: " +
1641 this._FBU.encoding + ")");
1642 return false;
1643 }
1644
1645 try {
1646 return decoder.decodeRect(this._FBU.x, this._FBU.y,
1647 this._FBU.width, this._FBU.height,
1648 this._sock, this._display,
1649 this._fb_depth);
1650 } catch (err) {
1651 this._fail("Error decoding rect: " + err);
1652 return false;
1653 }
1654 }
1655
1656 _updateContinuousUpdates() {
1657 if (!this._enabledContinuousUpdates) { return; }
1658
1659 RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
1660 this._fb_width, this._fb_height);
1661 }
1662
1663 _resize(width, height) {
1664 this._fb_width = width;
1665 this._fb_height = height;
1666
1667 this._display.resize(this._fb_width, this._fb_height);
1668
1669 // Adjust the visible viewport based on the new dimensions
1670 this._updateClip();
1671 this._updateScale();
1672
1673 this._updateContinuousUpdates();
1674 }
1675
1676 _xvpOp(ver, op) {
1677 if (this._rfb_xvp_ver < ver) { return; }
1678 Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
1679 RFB.messages.xvpOp(this._sock, ver, op);
1680 }
1681
1682 _updateCursor(rgba, hotx, hoty, w, h) {
1683 this._cursorImage = {
1684 rgbaPixels: rgba,
1685 hotx: hotx, hoty: hoty, w: w, h: h,
1686 };
1687 this._refreshCursor();
1688 }
1689
1690 _shouldShowDotCursor() {
1691 // Called when this._cursorImage is updated
1692 if (!this._showDotCursor) {
1693 // User does not want to see the dot, so...
1694 return false;
1695 }
1696
1697 // The dot should not be shown if the cursor is already visible,
1698 // i.e. contains at least one not-fully-transparent pixel.
1699 // So iterate through all alpha bytes in rgba and stop at the
1700 // first non-zero.
1701 for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) {
1702 if (this._cursorImage.rgbaPixels[i]) {
1703 return false;
1704 }
1705 }
1706
1707 // At this point, we know that the cursor is fully transparent, and
1708 // the user wants to see the dot instead of this.
1709 return true;
1710 }
1711
1712 _refreshCursor() {
1713 const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage;
1714 this._cursor.change(image.rgbaPixels,
1715 image.hotx, image.hoty,
1716 image.w, image.h
1717 );
1718 }
1719
1720 static genDES(password, challenge) {
1721 const passwd = [];
1722 for (let i = 0; i < password.length; i++) {
1723 passwd.push(password.charCodeAt(i));
1724 }
1725 return (new DES(passwd)).encrypt(challenge);
1726 }
1727 }
1728
1729 // Class Methods
1730 RFB.messages = {
1731 keyEvent(sock, keysym, down) {
1732 const buff = sock._sQ;
1733 const offset = sock._sQlen;
1734
1735 buff[offset] = 4; // msg-type
1736 buff[offset + 1] = down;
1737
1738 buff[offset + 2] = 0;
1739 buff[offset + 3] = 0;
1740
1741 buff[offset + 4] = (keysym >> 24);
1742 buff[offset + 5] = (keysym >> 16);
1743 buff[offset + 6] = (keysym >> 8);
1744 buff[offset + 7] = keysym;
1745
1746 sock._sQlen += 8;
1747 sock.flush();
1748 },
1749
1750 QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
1751 function getRFBkeycode(xt_scancode) {
1752 const upperByte = (keycode >> 8);
1753 const lowerByte = (keycode & 0x00ff);
1754 if (upperByte === 0xe0 && lowerByte < 0x7f) {
1755 return lowerByte | 0x80;
1756 }
1757 return xt_scancode;
1758 }
1759
1760 const buff = sock._sQ;
1761 const offset = sock._sQlen;
1762
1763 buff[offset] = 255; // msg-type
1764 buff[offset + 1] = 0; // sub msg-type
1765
1766 buff[offset + 2] = (down >> 8);
1767 buff[offset + 3] = down;
1768
1769 buff[offset + 4] = (keysym >> 24);
1770 buff[offset + 5] = (keysym >> 16);
1771 buff[offset + 6] = (keysym >> 8);
1772 buff[offset + 7] = keysym;
1773
1774 const RFBkeycode = getRFBkeycode(keycode);
1775
1776 buff[offset + 8] = (RFBkeycode >> 24);
1777 buff[offset + 9] = (RFBkeycode >> 16);
1778 buff[offset + 10] = (RFBkeycode >> 8);
1779 buff[offset + 11] = RFBkeycode;
1780
1781 sock._sQlen += 12;
1782 sock.flush();
1783 },
1784
1785 pointerEvent(sock, x, y, mask) {
1786 const buff = sock._sQ;
1787 const offset = sock._sQlen;
1788
1789 buff[offset] = 5; // msg-type
1790
1791 buff[offset + 1] = mask;
1792
1793 buff[offset + 2] = x >> 8;
1794 buff[offset + 3] = x;
1795
1796 buff[offset + 4] = y >> 8;
1797 buff[offset + 5] = y;
1798
1799 sock._sQlen += 6;
1800 sock.flush();
1801 },
1802
1803 // TODO(directxman12): make this unicode compatible?
1804 clientCutText(sock, text) {
1805 const buff = sock._sQ;
1806 const offset = sock._sQlen;
1807
1808 buff[offset] = 6; // msg-type
1809
1810 buff[offset + 1] = 0; // padding
1811 buff[offset + 2] = 0; // padding
1812 buff[offset + 3] = 0; // padding
1813
1814 let length = text.length;
1815
1816 buff[offset + 4] = length >> 24;
1817 buff[offset + 5] = length >> 16;
1818 buff[offset + 6] = length >> 8;
1819 buff[offset + 7] = length;
1820
1821 sock._sQlen += 8;
1822
1823 // We have to keep track of from where in the text we begin creating the
1824 // buffer for the flush in the next iteration.
1825 let textOffset = 0;
1826
1827 let remaining = length;
1828 while (remaining > 0) {
1829
1830 let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
1831 if (flushSize <= 0) {
1832 this._fail("Clipboard contents could not be sent");
1833 break;
1834 }
1835
1836 for (let i = 0; i < flushSize; i++) {
1837 buff[sock._sQlen + i] = text.charCodeAt(textOffset + i);
1838 }
1839
1840 sock._sQlen += flushSize;
1841 sock.flush();
1842
1843 remaining -= flushSize;
1844 textOffset += flushSize;
1845 }
1846 },
1847
1848 setDesktopSize(sock, width, height, id, flags) {
1849 const buff = sock._sQ;
1850 const offset = sock._sQlen;
1851
1852 buff[offset] = 251; // msg-type
1853 buff[offset + 1] = 0; // padding
1854 buff[offset + 2] = width >> 8; // width
1855 buff[offset + 3] = width;
1856 buff[offset + 4] = height >> 8; // height
1857 buff[offset + 5] = height;
1858
1859 buff[offset + 6] = 1; // number-of-screens
1860 buff[offset + 7] = 0; // padding
1861
1862 // screen array
1863 buff[offset + 8] = id >> 24; // id
1864 buff[offset + 9] = id >> 16;
1865 buff[offset + 10] = id >> 8;
1866 buff[offset + 11] = id;
1867 buff[offset + 12] = 0; // x-position
1868 buff[offset + 13] = 0;
1869 buff[offset + 14] = 0; // y-position
1870 buff[offset + 15] = 0;
1871 buff[offset + 16] = width >> 8; // width
1872 buff[offset + 17] = width;
1873 buff[offset + 18] = height >> 8; // height
1874 buff[offset + 19] = height;
1875 buff[offset + 20] = flags >> 24; // flags
1876 buff[offset + 21] = flags >> 16;
1877 buff[offset + 22] = flags >> 8;
1878 buff[offset + 23] = flags;
1879
1880 sock._sQlen += 24;
1881 sock.flush();
1882 },
1883
1884 clientFence(sock, flags, payload) {
1885 const buff = sock._sQ;
1886 const offset = sock._sQlen;
1887
1888 buff[offset] = 248; // msg-type
1889
1890 buff[offset + 1] = 0; // padding
1891 buff[offset + 2] = 0; // padding
1892 buff[offset + 3] = 0; // padding
1893
1894 buff[offset + 4] = flags >> 24; // flags
1895 buff[offset + 5] = flags >> 16;
1896 buff[offset + 6] = flags >> 8;
1897 buff[offset + 7] = flags;
1898
1899 const n = payload.length;
1900
1901 buff[offset + 8] = n; // length
1902
1903 for (let i = 0; i < n; i++) {
1904 buff[offset + 9 + i] = payload.charCodeAt(i);
1905 }
1906
1907 sock._sQlen += 9 + n;
1908 sock.flush();
1909 },
1910
1911 enableContinuousUpdates(sock, enable, x, y, width, height) {
1912 const buff = sock._sQ;
1913 const offset = sock._sQlen;
1914
1915 buff[offset] = 150; // msg-type
1916 buff[offset + 1] = enable; // enable-flag
1917
1918 buff[offset + 2] = x >> 8; // x
1919 buff[offset + 3] = x;
1920 buff[offset + 4] = y >> 8; // y
1921 buff[offset + 5] = y;
1922 buff[offset + 6] = width >> 8; // width
1923 buff[offset + 7] = width;
1924 buff[offset + 8] = height >> 8; // height
1925 buff[offset + 9] = height;
1926
1927 sock._sQlen += 10;
1928 sock.flush();
1929 },
1930
1931 pixelFormat(sock, depth, true_color) {
1932 const buff = sock._sQ;
1933 const offset = sock._sQlen;
1934
1935 let bpp;
1936
1937 if (depth > 16) {
1938 bpp = 32;
1939 } else if (depth > 8) {
1940 bpp = 16;
1941 } else {
1942 bpp = 8;
1943 }
1944
1945 const bits = Math.floor(depth/3);
1946
1947 buff[offset] = 0; // msg-type
1948
1949 buff[offset + 1] = 0; // padding
1950 buff[offset + 2] = 0; // padding
1951 buff[offset + 3] = 0; // padding
1952
1953 buff[offset + 4] = bpp; // bits-per-pixel
1954 buff[offset + 5] = depth; // depth
1955 buff[offset + 6] = 0; // little-endian
1956 buff[offset + 7] = true_color ? 1 : 0; // true-color
1957
1958 buff[offset + 8] = 0; // red-max
1959 buff[offset + 9] = (1 << bits) - 1; // red-max
1960
1961 buff[offset + 10] = 0; // green-max
1962 buff[offset + 11] = (1 << bits) - 1; // green-max
1963
1964 buff[offset + 12] = 0; // blue-max
1965 buff[offset + 13] = (1 << bits) - 1; // blue-max
1966
1967 buff[offset + 14] = bits * 2; // red-shift
1968 buff[offset + 15] = bits * 1; // green-shift
1969 buff[offset + 16] = bits * 0; // blue-shift
1970
1971 buff[offset + 17] = 0; // padding
1972 buff[offset + 18] = 0; // padding
1973 buff[offset + 19] = 0; // padding
1974
1975 sock._sQlen += 20;
1976 sock.flush();
1977 },
1978
1979 clientEncodings(sock, encodings) {
1980 const buff = sock._sQ;
1981 const offset = sock._sQlen;
1982
1983 buff[offset] = 2; // msg-type
1984 buff[offset + 1] = 0; // padding
1985
1986 buff[offset + 2] = encodings.length >> 8;
1987 buff[offset + 3] = encodings.length;
1988
1989 let j = offset + 4;
1990 for (let i = 0; i < encodings.length; i++) {
1991 const enc = encodings[i];
1992 buff[j] = enc >> 24;
1993 buff[j + 1] = enc >> 16;
1994 buff[j + 2] = enc >> 8;
1995 buff[j + 3] = enc;
1996
1997 j += 4;
1998 }
1999
2000 sock._sQlen += j - offset;
2001 sock.flush();
2002 },
2003
2004 fbUpdateRequest(sock, incremental, x, y, w, h) {
2005 const buff = sock._sQ;
2006 const offset = sock._sQlen;
2007
2008 if (typeof(x) === "undefined") { x = 0; }
2009 if (typeof(y) === "undefined") { y = 0; }
2010
2011 buff[offset] = 3; // msg-type
2012 buff[offset + 1] = incremental ? 1 : 0;
2013
2014 buff[offset + 2] = (x >> 8) & 0xFF;
2015 buff[offset + 3] = x & 0xFF;
2016
2017 buff[offset + 4] = (y >> 8) & 0xFF;
2018 buff[offset + 5] = y & 0xFF;
2019
2020 buff[offset + 6] = (w >> 8) & 0xFF;
2021 buff[offset + 7] = w & 0xFF;
2022
2023 buff[offset + 8] = (h >> 8) & 0xFF;
2024 buff[offset + 9] = h & 0xFF;
2025
2026 sock._sQlen += 10;
2027 sock.flush();
2028 },
2029
2030 xvpOp(sock, ver, op) {
2031 const buff = sock._sQ;
2032 const offset = sock._sQlen;
2033
2034 buff[offset] = 250; // msg-type
2035 buff[offset + 1] = 0; // padding
2036
2037 buff[offset + 2] = ver;
2038 buff[offset + 3] = op;
2039
2040 sock._sQlen += 4;
2041 sock.flush();
2042 }
2043 };
2044
2045 RFB.cursors = {
2046 none: {
2047 rgbaPixels: new Uint8Array(),
2048 w: 0, h: 0,
2049 hotx: 0, hoty: 0,
2050 },
2051
2052 dot: {
2053 /* eslint-disable indent */
2054 rgbaPixels: new Uint8Array([
2055 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2056 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
2057 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2058 ]),
2059 /* eslint-enable indent */
2060 w: 3, h: 3,
2061 hotx: 1, hoty: 1,
2062 }
2063 };