]> git.proxmox.com Git - mirror_novnc.git/blob - core/rfb.js
Convert DES into a class
[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 const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
35
36 export default class RFB extends EventTargetMixin {
37 constructor(target, url, options) {
38 if (!target) {
39 throw new Error("Must specify target");
40 }
41 if (!url) {
42 throw new Error("Must specify URL");
43 }
44
45 super();
46
47 this._target = target;
48 this._url = url;
49
50 // Connection details
51 options = options || {};
52 this._rfb_credentials = options.credentials || {};
53 this._shared = 'shared' in options ? !!options.shared : true;
54 this._repeaterID = options.repeaterID || '';
55 this._showDotCursor = options.showDotCursor || false;
56
57 // Internal state
58 this._rfb_connection_state = '';
59 this._rfb_init_state = '';
60 this._rfb_auth_scheme = -1;
61 this._rfb_clean_disconnect = true;
62
63 // Server capabilities
64 this._rfb_version = 0;
65 this._rfb_max_version = 3.8;
66 this._rfb_tightvnc = false;
67 this._rfb_xvp_ver = 0;
68
69 this._fb_width = 0;
70 this._fb_height = 0;
71
72 this._fb_name = "";
73
74 this._capabilities = { power: false };
75
76 this._supportsFence = false;
77
78 this._supportsContinuousUpdates = false;
79 this._enabledContinuousUpdates = false;
80
81 this._supportsSetDesktopSize = false;
82 this._screen_id = 0;
83 this._screen_flags = 0;
84
85 this._qemuExtKeyEventSupported = false;
86
87 // Internal objects
88 this._sock = null; // Websock object
89 this._display = null; // Display object
90 this._flushing = false; // Display flushing state
91 this._keyboard = null; // Keyboard input handler object
92 this._mouse = null; // Mouse input handler object
93
94 // Timers
95 this._disconnTimer = null; // disconnection timer
96 this._resizeTimeout = null; // resize rate limiting
97
98 // Decoder states
99 this._decoders = {};
100
101 this._FBU = {
102 rects: 0,
103 x: 0,
104 y: 0,
105 width: 0,
106 height: 0,
107 encoding: null,
108 };
109
110 // Mouse state
111 this._mouse_buttonMask = 0;
112 this._mouse_arr = [];
113 this._viewportDragging = false;
114 this._viewportDragPos = {};
115 this._viewportHasMoved = false;
116
117 // Bound event handlers
118 this._eventHandlers = {
119 focusCanvas: this._focusCanvas.bind(this),
120 windowResize: this._windowResize.bind(this),
121 };
122
123 // main setup
124 Log.Debug(">> RFB.constructor");
125
126 // Create DOM elements
127 this._screen = document.createElement('div');
128 this._screen.style.display = 'flex';
129 this._screen.style.width = '100%';
130 this._screen.style.height = '100%';
131 this._screen.style.overflow = 'auto';
132 this._screen.style.background = DEFAULT_BACKGROUND;
133 this._canvas = document.createElement('canvas');
134 this._canvas.style.margin = 'auto';
135 // Some browsers add an outline on focus
136 this._canvas.style.outline = 'none';
137 // IE miscalculates width without this :(
138 this._canvas.style.flexShrink = '0';
139 this._canvas.width = 0;
140 this._canvas.height = 0;
141 this._canvas.tabIndex = -1;
142 this._screen.appendChild(this._canvas);
143
144 // Cursor
145 this._cursor = new Cursor();
146
147 // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
148 // it. Result: no cursor at all until a window border or an edit field
149 // is hit blindly. But there are also VNC servers that draw the cursor
150 // in the framebuffer and don't send the empty local cursor. There is
151 // no way to satisfy both sides.
152 //
153 // The spec is unclear on this "initial cursor" issue. Many other
154 // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
155 // initial cursor instead.
156 this._cursorImage = RFB.cursors.none;
157
158 // populate decoder array with objects
159 this._decoders[encodings.encodingRaw] = new RawDecoder();
160 this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
161 this._decoders[encodings.encodingRRE] = new RREDecoder();
162 this._decoders[encodings.encodingHextile] = new HextileDecoder();
163 this._decoders[encodings.encodingTight] = new TightDecoder();
164 this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
165
166 // NB: nothing that needs explicit teardown should be done
167 // before this point, since this can throw an exception
168 try {
169 this._display = new Display(this._canvas);
170 } catch (exc) {
171 Log.Error("Display exception: " + exc);
172 throw exc;
173 }
174 this._display.onflush = this._onFlush.bind(this);
175 this._display.clear();
176
177 this._keyboard = new Keyboard(this._canvas);
178 this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
179
180 this._mouse = new Mouse(this._canvas);
181 this._mouse.onmousebutton = this._handleMouseButton.bind(this);
182 this._mouse.onmousemove = this._handleMouseMove.bind(this);
183
184 this._sock = new Websock();
185 this._sock.on('message', () => {
186 this._handle_message();
187 });
188 this._sock.on('open', () => {
189 if ((this._rfb_connection_state === 'connecting') &&
190 (this._rfb_init_state === '')) {
191 this._rfb_init_state = 'ProtocolVersion';
192 Log.Debug("Starting VNC handshake");
193 } else {
194 this._fail("Unexpected server connection while " +
195 this._rfb_connection_state);
196 }
197 });
198 this._sock.on('close', (e) => {
199 Log.Debug("WebSocket on-close event");
200 let msg = "";
201 if (e.code) {
202 msg = "(code: " + e.code;
203 if (e.reason) {
204 msg += ", reason: " + e.reason;
205 }
206 msg += ")";
207 }
208 switch (this._rfb_connection_state) {
209 case 'connecting':
210 this._fail("Connection closed " + msg);
211 break;
212 case 'connected':
213 // Handle disconnects that were initiated server-side
214 this._updateConnectionState('disconnecting');
215 this._updateConnectionState('disconnected');
216 break;
217 case 'disconnecting':
218 // Normal disconnection path
219 this._updateConnectionState('disconnected');
220 break;
221 case 'disconnected':
222 this._fail("Unexpected server disconnect " +
223 "when already disconnected " + msg);
224 break;
225 default:
226 this._fail("Unexpected server disconnect before connecting " +
227 msg);
228 break;
229 }
230 this._sock.off('close');
231 });
232 this._sock.on('error', e => Log.Warn("WebSocket on-error event"));
233
234 // Slight delay of the actual connection so that the caller has
235 // time to set up callbacks
236 setTimeout(this._updateConnectionState.bind(this, 'connecting'));
237
238 Log.Debug("<< RFB.constructor");
239
240 // ===== PROPERTIES =====
241
242 this.dragViewport = false;
243 this.focusOnClick = true;
244
245 this._viewOnly = false;
246 this._clipViewport = false;
247 this._scaleViewport = false;
248 this._resizeSession = false;
249 }
250
251 // ===== PROPERTIES =====
252
253 get viewOnly() { return this._viewOnly; }
254 set viewOnly(viewOnly) {
255 this._viewOnly = viewOnly;
256
257 if (this._rfb_connection_state === "connecting" ||
258 this._rfb_connection_state === "connected") {
259 if (viewOnly) {
260 this._keyboard.ungrab();
261 this._mouse.ungrab();
262 } else {
263 this._keyboard.grab();
264 this._mouse.grab();
265 }
266 }
267 }
268
269 get capabilities() { return this._capabilities; }
270
271 get touchButton() { return this._mouse.touchButton; }
272 set touchButton(button) { this._mouse.touchButton = button; }
273
274 get clipViewport() { return this._clipViewport; }
275 set clipViewport(viewport) {
276 this._clipViewport = viewport;
277 this._updateClip();
278 }
279
280 get scaleViewport() { return this._scaleViewport; }
281 set scaleViewport(scale) {
282 this._scaleViewport = scale;
283 // Scaling trumps clipping, so we may need to adjust
284 // clipping when enabling or disabling scaling
285 if (scale && this._clipViewport) {
286 this._updateClip();
287 }
288 this._updateScale();
289 if (!scale && this._clipViewport) {
290 this._updateClip();
291 }
292 }
293
294 get resizeSession() { return this._resizeSession; }
295 set resizeSession(resize) {
296 this._resizeSession = resize;
297 if (resize) {
298 this._requestRemoteResize();
299 }
300 }
301
302 get showDotCursor() { return this._showDotCursor; }
303 set showDotCursor(show) {
304 this._showDotCursor = show;
305 this._refreshCursor();
306 }
307
308 get background() { return this._screen.style.background; }
309 set background(cssValue) { this._screen.style.background = cssValue; }
310
311 // ===== PUBLIC METHODS =====
312
313 disconnect() {
314 this._updateConnectionState('disconnecting');
315 this._sock.off('error');
316 this._sock.off('message');
317 this._sock.off('open');
318 }
319
320 sendCredentials(creds) {
321 this._rfb_credentials = creds;
322 setTimeout(this._init_msg.bind(this), 0);
323 }
324
325 sendCtrlAltDel() {
326 if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
327 Log.Info("Sending Ctrl-Alt-Del");
328
329 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
330 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
331 this.sendKey(KeyTable.XK_Delete, "Delete", true);
332 this.sendKey(KeyTable.XK_Delete, "Delete", false);
333 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
334 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
335 }
336
337 machineShutdown() {
338 this._xvpOp(1, 2);
339 }
340
341 machineReboot() {
342 this._xvpOp(1, 3);
343 }
344
345 machineReset() {
346 this._xvpOp(1, 4);
347 }
348
349 // Send a key press. If 'down' is not specified then send a down key
350 // followed by an up key.
351 sendKey(keysym, code, down) {
352 if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
353
354 if (down === undefined) {
355 this.sendKey(keysym, code, true);
356 this.sendKey(keysym, code, false);
357 return;
358 }
359
360 const scancode = XtScancode[code];
361
362 if (this._qemuExtKeyEventSupported && scancode) {
363 // 0 is NoSymbol
364 keysym = keysym || 0;
365
366 Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
367
368 RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
369 } else {
370 if (!keysym) {
371 return;
372 }
373 Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
374 RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
375 }
376 }
377
378 focus() {
379 this._canvas.focus();
380 }
381
382 blur() {
383 this._canvas.blur();
384 }
385
386 clipboardPasteFrom(text) {
387 if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
388 RFB.messages.clientCutText(this._sock, text);
389 }
390
391 // ===== PRIVATE METHODS =====
392
393 _connect() {
394 Log.Debug(">> RFB.connect");
395
396 Log.Info("connecting to " + this._url);
397
398 try {
399 // WebSocket.onopen transitions to the RFB init states
400 this._sock.open(this._url, ['binary']);
401 } catch (e) {
402 if (e.name === 'SyntaxError') {
403 this._fail("Invalid host or port (" + e + ")");
404 } else {
405 this._fail("Error when opening socket (" + e + ")");
406 }
407 }
408
409 // Make our elements part of the page
410 this._target.appendChild(this._screen);
411
412 this._cursor.attach(this._canvas);
413 this._refreshCursor();
414
415 // Monitor size changes of the screen
416 // FIXME: Use ResizeObserver, or hidden overflow
417 window.addEventListener('resize', this._eventHandlers.windowResize);
418
419 // Always grab focus on some kind of click event
420 this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
421 this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);
422
423 Log.Debug("<< RFB.connect");
424 }
425
426 _disconnect() {
427 Log.Debug(">> RFB.disconnect");
428 this._cursor.detach();
429 this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
430 this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
431 window.removeEventListener('resize', this._eventHandlers.windowResize);
432 this._keyboard.ungrab();
433 this._mouse.ungrab();
434 this._sock.close();
435 try {
436 this._target.removeChild(this._screen);
437 } catch (e) {
438 if (e.name === 'NotFoundError') {
439 // Some cases where the initial connection fails
440 // can disconnect before the _screen is created
441 } else {
442 throw e;
443 }
444 }
445 clearTimeout(this._resizeTimeout);
446 Log.Debug("<< RFB.disconnect");
447 }
448
449 _focusCanvas(event) {
450 // Respect earlier handlers' request to not do side-effects
451 if (event.defaultPrevented) {
452 return;
453 }
454
455 if (!this.focusOnClick) {
456 return;
457 }
458
459 this.focus();
460 }
461
462 _windowResize(event) {
463 // If the window resized then our screen element might have
464 // as well. Update the viewport dimensions.
465 window.requestAnimationFrame(() => {
466 this._updateClip();
467 this._updateScale();
468 });
469
470 if (this._resizeSession) {
471 // Request changing the resolution of the remote display to
472 // the size of the local browser viewport.
473
474 // In order to not send multiple requests before the browser-resize
475 // is finished we wait 0.5 seconds before sending the request.
476 clearTimeout(this._resizeTimeout);
477 this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500);
478 }
479 }
480
481 // Update state of clipping in Display object, and make sure the
482 // configured viewport matches the current screen size
483 _updateClip() {
484 const cur_clip = this._display.clipViewport;
485 let new_clip = this._clipViewport;
486
487 if (this._scaleViewport) {
488 // Disable viewport clipping if we are scaling
489 new_clip = false;
490 }
491
492 if (cur_clip !== new_clip) {
493 this._display.clipViewport = new_clip;
494 }
495
496 if (new_clip) {
497 // When clipping is enabled, the screen is limited to
498 // the size of the container.
499 const size = this._screenSize();
500 this._display.viewportChangeSize(size.w, size.h);
501 this._fixScrollbars();
502 }
503 }
504
505 _updateScale() {
506 if (!this._scaleViewport) {
507 this._display.scale = 1.0;
508 } else {
509 const size = this._screenSize();
510 this._display.autoscale(size.w, size.h);
511 }
512 this._fixScrollbars();
513 }
514
515 // Requests a change of remote desktop size. This message is an extension
516 // and may only be sent if we have received an ExtendedDesktopSize message
517 _requestRemoteResize() {
518 clearTimeout(this._resizeTimeout);
519 this._resizeTimeout = null;
520
521 if (!this._resizeSession || this._viewOnly ||
522 !this._supportsSetDesktopSize) {
523 return;
524 }
525
526 const size = this._screenSize();
527 RFB.messages.setDesktopSize(this._sock,
528 Math.floor(size.w), Math.floor(size.h),
529 this._screen_id, this._screen_flags);
530
531 Log.Debug('Requested new desktop size: ' +
532 size.w + 'x' + size.h);
533 }
534
535 // Gets the the size of the available screen
536 _screenSize() {
537 let r = this._screen.getBoundingClientRect();
538 return { w: r.width, h: r.height };
539 }
540
541 _fixScrollbars() {
542 // This is a hack because Chrome screws up the calculation
543 // for when scrollbars are needed. So to fix it we temporarily
544 // toggle them off and on.
545 const orig = this._screen.style.overflow;
546 this._screen.style.overflow = 'hidden';
547 // Force Chrome to recalculate the layout by asking for
548 // an element's dimensions
549 this._screen.getBoundingClientRect();
550 this._screen.style.overflow = orig;
551 }
552
553 /*
554 * Connection states:
555 * connecting
556 * connected
557 * disconnecting
558 * disconnected - permanent state
559 */
560 _updateConnectionState(state) {
561 const oldstate = this._rfb_connection_state;
562
563 if (state === oldstate) {
564 Log.Debug("Already in state '" + state + "', ignoring");
565 return;
566 }
567
568 // The 'disconnected' state is permanent for each RFB object
569 if (oldstate === 'disconnected') {
570 Log.Error("Tried changing state of a disconnected RFB object");
571 return;
572 }
573
574 // Ensure proper transitions before doing anything
575 switch (state) {
576 case 'connected':
577 if (oldstate !== 'connecting') {
578 Log.Error("Bad transition to connected state, " +
579 "previous connection state: " + oldstate);
580 return;
581 }
582 break;
583
584 case 'disconnected':
585 if (oldstate !== 'disconnecting') {
586 Log.Error("Bad transition to disconnected state, " +
587 "previous connection state: " + oldstate);
588 return;
589 }
590 break;
591
592 case 'connecting':
593 if (oldstate !== '') {
594 Log.Error("Bad transition to connecting state, " +
595 "previous connection state: " + oldstate);
596 return;
597 }
598 break;
599
600 case 'disconnecting':
601 if (oldstate !== 'connected' && oldstate !== 'connecting') {
602 Log.Error("Bad transition to disconnecting state, " +
603 "previous connection state: " + oldstate);
604 return;
605 }
606 break;
607
608 default:
609 Log.Error("Unknown connection state: " + state);
610 return;
611 }
612
613 // State change actions
614
615 this._rfb_connection_state = state;
616
617 Log.Debug("New state '" + state + "', was '" + oldstate + "'.");
618
619 if (this._disconnTimer && state !== 'disconnecting') {
620 Log.Debug("Clearing disconnect timer");
621 clearTimeout(this._disconnTimer);
622 this._disconnTimer = null;
623
624 // make sure we don't get a double event
625 this._sock.off('close');
626 }
627
628 switch (state) {
629 case 'connecting':
630 this._connect();
631 break;
632
633 case 'connected':
634 this.dispatchEvent(new CustomEvent("connect", { detail: {} }));
635 break;
636
637 case 'disconnecting':
638 this._disconnect();
639
640 this._disconnTimer = setTimeout(() => {
641 Log.Error("Disconnection timed out.");
642 this._updateConnectionState('disconnected');
643 }, DISCONNECT_TIMEOUT * 1000);
644 break;
645
646 case 'disconnected':
647 this.dispatchEvent(new CustomEvent(
648 "disconnect", { detail:
649 { clean: this._rfb_clean_disconnect } }));
650 break;
651 }
652 }
653
654 /* Print errors and disconnect
655 *
656 * The parameter 'details' is used for information that
657 * should be logged but not sent to the user interface.
658 */
659 _fail(details) {
660 switch (this._rfb_connection_state) {
661 case 'disconnecting':
662 Log.Error("Failed when disconnecting: " + details);
663 break;
664 case 'connected':
665 Log.Error("Failed while connected: " + details);
666 break;
667 case 'connecting':
668 Log.Error("Failed when connecting: " + details);
669 break;
670 default:
671 Log.Error("RFB failure: " + details);
672 break;
673 }
674 this._rfb_clean_disconnect = false; //This is sent to the UI
675
676 // Transition to disconnected without waiting for socket to close
677 this._updateConnectionState('disconnecting');
678 this._updateConnectionState('disconnected');
679
680 return false;
681 }
682
683 _setCapability(cap, val) {
684 this._capabilities[cap] = val;
685 this.dispatchEvent(new CustomEvent("capabilities",
686 { detail: { capabilities: this._capabilities } }));
687 }
688
689 _handle_message() {
690 if (this._sock.rQlen === 0) {
691 Log.Warn("handle_message called on an empty receive queue");
692 return;
693 }
694
695 switch (this._rfb_connection_state) {
696 case 'disconnected':
697 Log.Error("Got data while disconnected");
698 break;
699 case 'connected':
700 while (true) {
701 if (this._flushing) {
702 break;
703 }
704 if (!this._normal_msg()) {
705 break;
706 }
707 if (this._sock.rQlen === 0) {
708 break;
709 }
710 }
711 break;
712 default:
713 this._init_msg();
714 break;
715 }
716 }
717
718 _handleKeyEvent(keysym, code, down) {
719 this.sendKey(keysym, code, down);
720 }
721
722 _handleMouseButton(x, y, down, bmask) {
723 if (down) {
724 this._mouse_buttonMask |= bmask;
725 } else {
726 this._mouse_buttonMask &= ~bmask;
727 }
728
729 if (this.dragViewport) {
730 if (down && !this._viewportDragging) {
731 this._viewportDragging = true;
732 this._viewportDragPos = {'x': x, 'y': y};
733 this._viewportHasMoved = false;
734
735 // Skip sending mouse events
736 return;
737 } else {
738 this._viewportDragging = false;
739
740 // If we actually performed a drag then we are done
741 // here and should not send any mouse events
742 if (this._viewportHasMoved) {
743 return;
744 }
745
746 // Otherwise we treat this as a mouse click event.
747 // Send the button down event here, as the button up
748 // event is sent at the end of this function.
749 RFB.messages.pointerEvent(this._sock,
750 this._display.absX(x),
751 this._display.absY(y),
752 bmask);
753 }
754 }
755
756 if (this._viewOnly) { return; } // View only, skip mouse events
757
758 if (this._rfb_connection_state !== 'connected') { return; }
759 RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
760 }
761
762 _handleMouseMove(x, y) {
763 if (this._viewportDragging) {
764 const deltaX = this._viewportDragPos.x - x;
765 const deltaY = this._viewportDragPos.y - y;
766
767 if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
768 Math.abs(deltaY) > dragThreshold)) {
769 this._viewportHasMoved = true;
770
771 this._viewportDragPos = {'x': x, 'y': y};
772 this._display.viewportChangePos(deltaX, deltaY);
773 }
774
775 // Skip sending mouse events
776 return;
777 }
778
779 if (this._viewOnly) { return; } // View only, skip mouse events
780
781 if (this._rfb_connection_state !== 'connected') { return; }
782 RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
783 }
784
785 // Message Handlers
786
787 _negotiate_protocol_version() {
788 if (this._sock.rQwait("version", 12)) {
789 return false;
790 }
791
792 const sversion = this._sock.rQshiftStr(12).substr(4, 7);
793 Log.Info("Server ProtocolVersion: " + sversion);
794 let is_repeater = 0;
795 switch (sversion) {
796 case "000.000": // UltraVNC repeater
797 is_repeater = 1;
798 break;
799 case "003.003":
800 case "003.006": // UltraVNC
801 case "003.889": // Apple Remote Desktop
802 this._rfb_version = 3.3;
803 break;
804 case "003.007":
805 this._rfb_version = 3.7;
806 break;
807 case "003.008":
808 case "004.000": // Intel AMT KVM
809 case "004.001": // RealVNC 4.6
810 case "005.000": // RealVNC 5.3
811 this._rfb_version = 3.8;
812 break;
813 default:
814 return this._fail("Invalid server version " + sversion);
815 }
816
817 if (is_repeater) {
818 let repeaterID = "ID:" + this._repeaterID;
819 while (repeaterID.length < 250) {
820 repeaterID += "\0";
821 }
822 this._sock.send_string(repeaterID);
823 return true;
824 }
825
826 if (this._rfb_version > this._rfb_max_version) {
827 this._rfb_version = this._rfb_max_version;
828 }
829
830 const cversion = "00" + parseInt(this._rfb_version, 10) +
831 ".00" + ((this._rfb_version * 10) % 10);
832 this._sock.send_string("RFB " + cversion + "\n");
833 Log.Debug('Sent ProtocolVersion: ' + cversion);
834
835 this._rfb_init_state = 'Security';
836 }
837
838 _negotiate_security() {
839 // Polyfill since IE and PhantomJS doesn't have
840 // TypedArray.includes()
841 function includes(item, array) {
842 for (let i = 0; i < array.length; i++) {
843 if (array[i] === item) {
844 return true;
845 }
846 }
847 return false;
848 }
849
850 if (this._rfb_version >= 3.7) {
851 // Server sends supported list, client decides
852 const num_types = this._sock.rQshift8();
853 if (this._sock.rQwait("security type", num_types, 1)) { return false; }
854
855 if (num_types === 0) {
856 this._rfb_init_state = "SecurityReason";
857 this._security_context = "no security types";
858 this._security_status = 1;
859 return this._init_msg();
860 }
861
862 const types = this._sock.rQshiftBytes(num_types);
863 Log.Debug("Server security types: " + types);
864
865 // Look for each auth in preferred order
866 if (includes(1, types)) {
867 this._rfb_auth_scheme = 1; // None
868 } else if (includes(22, types)) {
869 this._rfb_auth_scheme = 22; // XVP
870 } else if (includes(16, types)) {
871 this._rfb_auth_scheme = 16; // Tight
872 } else if (includes(2, types)) {
873 this._rfb_auth_scheme = 2; // VNC Auth
874 } else {
875 return this._fail("Unsupported security types (types: " + types + ")");
876 }
877
878 this._sock.send([this._rfb_auth_scheme]);
879 } else {
880 // Server decides
881 if (this._sock.rQwait("security scheme", 4)) { return false; }
882 this._rfb_auth_scheme = this._sock.rQshift32();
883
884 if (this._rfb_auth_scheme == 0) {
885 this._rfb_init_state = "SecurityReason";
886 this._security_context = "authentication scheme";
887 this._security_status = 1;
888 return this._init_msg();
889 }
890 }
891
892 this._rfb_init_state = 'Authentication';
893 Log.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme);
894
895 return this._init_msg(); // jump to authentication
896 }
897
898 _handle_security_reason() {
899 if (this._sock.rQwait("reason length", 4)) {
900 return false;
901 }
902 const strlen = this._sock.rQshift32();
903 let reason = "";
904
905 if (strlen > 0) {
906 if (this._sock.rQwait("reason", strlen, 4)) { return false; }
907 reason = this._sock.rQshiftStr(strlen);
908 }
909
910 if (reason !== "") {
911 this.dispatchEvent(new CustomEvent(
912 "securityfailure",
913 { detail: { status: this._security_status,
914 reason: reason } }));
915
916 return this._fail("Security negotiation failed on " +
917 this._security_context +
918 " (reason: " + reason + ")");
919 } else {
920 this.dispatchEvent(new CustomEvent(
921 "securityfailure",
922 { detail: { status: this._security_status } }));
923
924 return this._fail("Security negotiation failed on " +
925 this._security_context);
926 }
927 }
928
929 // authentication
930 _negotiate_xvp_auth() {
931 if (!this._rfb_credentials.username ||
932 !this._rfb_credentials.password ||
933 !this._rfb_credentials.target) {
934 this.dispatchEvent(new CustomEvent(
935 "credentialsrequired",
936 { detail: { types: ["username", "password", "target"] } }));
937 return false;
938 }
939
940 const xvp_auth_str = String.fromCharCode(this._rfb_credentials.username.length) +
941 String.fromCharCode(this._rfb_credentials.target.length) +
942 this._rfb_credentials.username +
943 this._rfb_credentials.target;
944 this._sock.send_string(xvp_auth_str);
945 this._rfb_auth_scheme = 2;
946 return this._negotiate_authentication();
947 }
948
949 _negotiate_std_vnc_auth() {
950 if (this._sock.rQwait("auth challenge", 16)) { return false; }
951
952 if (!this._rfb_credentials.password) {
953 this.dispatchEvent(new CustomEvent(
954 "credentialsrequired",
955 { detail: { types: ["password"] } }));
956 return false;
957 }
958
959 // TODO(directxman12): make genDES not require an Array
960 const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
961 const response = RFB.genDES(this._rfb_credentials.password, challenge);
962 this._sock.send(response);
963 this._rfb_init_state = "SecurityResult";
964 return true;
965 }
966
967 _negotiate_tight_tunnels(numTunnels) {
968 const clientSupportedTunnelTypes = {
969 0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
970 };
971 const serverSupportedTunnelTypes = {};
972 // receive tunnel capabilities
973 for (let i = 0; i < numTunnels; i++) {
974 const cap_code = this._sock.rQshift32();
975 const cap_vendor = this._sock.rQshiftStr(4);
976 const cap_signature = this._sock.rQshiftStr(8);
977 serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature };
978 }
979
980 Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes);
981
982 // Siemens touch panels have a VNC server that supports NOTUNNEL,
983 // but forgets to advertise it. Try to detect such servers by
984 // looking for their custom tunnel type.
985 if (serverSupportedTunnelTypes[1] &&
986 (serverSupportedTunnelTypes[1].vendor === "SICR") &&
987 (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) {
988 Log.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
989 serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };
990 }
991
992 // choose the notunnel type
993 if (serverSupportedTunnelTypes[0]) {
994 if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
995 serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
996 return this._fail("Client's tunnel type had the incorrect " +
997 "vendor or signature");
998 }
999 Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
1000 this._sock.send([0, 0, 0, 0]); // use NOTUNNEL
1001 return false; // wait until we receive the sub auth count to continue
1002 } else {
1003 return this._fail("Server wanted tunnels, but doesn't support " +
1004 "the notunnel type");
1005 }
1006 }
1007
1008 _negotiate_tight_auth() {
1009 if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation
1010 if (this._sock.rQwait("num tunnels", 4)) { return false; }
1011 const numTunnels = this._sock.rQshift32();
1012 if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
1013
1014 this._rfb_tightvnc = true;
1015
1016 if (numTunnels > 0) {
1017 this._negotiate_tight_tunnels(numTunnels);
1018 return false; // wait until we receive the sub auth to continue
1019 }
1020 }
1021
1022 // second pass, do the sub-auth negotiation
1023 if (this._sock.rQwait("sub auth count", 4)) { return false; }
1024 const subAuthCount = this._sock.rQshift32();
1025 if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected
1026 this._rfb_init_state = 'SecurityResult';
1027 return true;
1028 }
1029
1030 if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
1031
1032 const clientSupportedTypes = {
1033 'STDVNOAUTH__': 1,
1034 'STDVVNCAUTH_': 2
1035 };
1036
1037 const serverSupportedTypes = [];
1038
1039 for (let i = 0; i < subAuthCount; i++) {
1040 this._sock.rQshift32(); // capNum
1041 const capabilities = this._sock.rQshiftStr(12);
1042 serverSupportedTypes.push(capabilities);
1043 }
1044
1045 Log.Debug("Server Tight authentication types: " + serverSupportedTypes);
1046
1047 for (let authType in clientSupportedTypes) {
1048 if (serverSupportedTypes.indexOf(authType) != -1) {
1049 this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
1050 Log.Debug("Selected authentication type: " + authType);
1051
1052 switch (authType) {
1053 case 'STDVNOAUTH__': // no auth
1054 this._rfb_init_state = 'SecurityResult';
1055 return true;
1056 case 'STDVVNCAUTH_': // VNC auth
1057 this._rfb_auth_scheme = 2;
1058 return this._init_msg();
1059 default:
1060 return this._fail("Unsupported tiny auth scheme " +
1061 "(scheme: " + authType + ")");
1062 }
1063 }
1064 }
1065
1066 return this._fail("No supported sub-auth types!");
1067 }
1068
1069 _negotiate_authentication() {
1070 switch (this._rfb_auth_scheme) {
1071 case 1: // no auth
1072 if (this._rfb_version >= 3.8) {
1073 this._rfb_init_state = 'SecurityResult';
1074 return true;
1075 }
1076 this._rfb_init_state = 'ClientInitialisation';
1077 return this._init_msg();
1078
1079 case 22: // XVP auth
1080 return this._negotiate_xvp_auth();
1081
1082 case 2: // VNC authentication
1083 return this._negotiate_std_vnc_auth();
1084
1085 case 16: // TightVNC Security Type
1086 return this._negotiate_tight_auth();
1087
1088 default:
1089 return this._fail("Unsupported auth scheme (scheme: " +
1090 this._rfb_auth_scheme + ")");
1091 }
1092 }
1093
1094 _handle_security_result() {
1095 if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
1096
1097 const status = this._sock.rQshift32();
1098
1099 if (status === 0) { // OK
1100 this._rfb_init_state = 'ClientInitialisation';
1101 Log.Debug('Authentication OK');
1102 return this._init_msg();
1103 } else {
1104 if (this._rfb_version >= 3.8) {
1105 this._rfb_init_state = "SecurityReason";
1106 this._security_context = "security result";
1107 this._security_status = status;
1108 return this._init_msg();
1109 } else {
1110 this.dispatchEvent(new CustomEvent(
1111 "securityfailure",
1112 { detail: { status: status } }));
1113
1114 return this._fail("Security handshake failed");
1115 }
1116 }
1117 }
1118
1119 _negotiate_server_init() {
1120 if (this._sock.rQwait("server initialization", 24)) { return false; }
1121
1122 /* Screen size */
1123 const width = this._sock.rQshift16();
1124 const height = this._sock.rQshift16();
1125
1126 /* PIXEL_FORMAT */
1127 const bpp = this._sock.rQshift8();
1128 const depth = this._sock.rQshift8();
1129 const big_endian = this._sock.rQshift8();
1130 const true_color = this._sock.rQshift8();
1131
1132 const red_max = this._sock.rQshift16();
1133 const green_max = this._sock.rQshift16();
1134 const blue_max = this._sock.rQshift16();
1135 const red_shift = this._sock.rQshift8();
1136 const green_shift = this._sock.rQshift8();
1137 const blue_shift = this._sock.rQshift8();
1138 this._sock.rQskipBytes(3); // padding
1139
1140 // NB(directxman12): we don't want to call any callbacks or print messages until
1141 // *after* we're past the point where we could backtrack
1142
1143 /* Connection name/title */
1144 const name_length = this._sock.rQshift32();
1145 if (this._sock.rQwait('server init name', name_length, 24)) { return false; }
1146 this._fb_name = decodeUTF8(this._sock.rQshiftStr(name_length));
1147
1148 if (this._rfb_tightvnc) {
1149 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; }
1150 // In TightVNC mode, ServerInit message is extended
1151 const numServerMessages = this._sock.rQshift16();
1152 const numClientMessages = this._sock.rQshift16();
1153 const numEncodings = this._sock.rQshift16();
1154 this._sock.rQskipBytes(2); // padding
1155
1156 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
1157 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; }
1158
1159 // we don't actually do anything with the capability information that TIGHT sends,
1160 // so we just skip the all of this.
1161
1162 // TIGHT server message capabilities
1163 this._sock.rQskipBytes(16 * numServerMessages);
1164
1165 // TIGHT client message capabilities
1166 this._sock.rQskipBytes(16 * numClientMessages);
1167
1168 // TIGHT encoding capabilities
1169 this._sock.rQskipBytes(16 * numEncodings);
1170 }
1171
1172 // NB(directxman12): these are down here so that we don't run them multiple times
1173 // if we backtrack
1174 Log.Info("Screen: " + width + "x" + height +
1175 ", bpp: " + bpp + ", depth: " + depth +
1176 ", big_endian: " + big_endian +
1177 ", true_color: " + true_color +
1178 ", red_max: " + red_max +
1179 ", green_max: " + green_max +
1180 ", blue_max: " + blue_max +
1181 ", red_shift: " + red_shift +
1182 ", green_shift: " + green_shift +
1183 ", blue_shift: " + blue_shift);
1184
1185 if (big_endian !== 0) {
1186 Log.Warn("Server native endian is not little endian");
1187 }
1188
1189 if (red_shift !== 16) {
1190 Log.Warn("Server native red-shift is not 16");
1191 }
1192
1193 if (blue_shift !== 0) {
1194 Log.Warn("Server native blue-shift is not 0");
1195 }
1196
1197 // we're past the point where we could backtrack, so it's safe to call this
1198 this.dispatchEvent(new CustomEvent(
1199 "desktopname",
1200 { detail: { name: this._fb_name } }));
1201
1202 this._resize(width, height);
1203
1204 if (!this._viewOnly) { this._keyboard.grab(); }
1205 if (!this._viewOnly) { this._mouse.grab(); }
1206
1207 this._fb_depth = 24;
1208
1209 if (this._fb_name === "Intel(r) AMT KVM") {
1210 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
1211 this._fb_depth = 8;
1212 }
1213
1214 RFB.messages.pixelFormat(this._sock, this._fb_depth, true);
1215 this._sendEncodings();
1216 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height);
1217
1218 this._updateConnectionState('connected');
1219 return true;
1220 }
1221
1222 _sendEncodings() {
1223 const encs = [];
1224
1225 // In preference order
1226 encs.push(encodings.encodingCopyRect);
1227 // Only supported with full depth support
1228 if (this._fb_depth == 24) {
1229 encs.push(encodings.encodingTight);
1230 encs.push(encodings.encodingTightPNG);
1231 encs.push(encodings.encodingHextile);
1232 encs.push(encodings.encodingRRE);
1233 }
1234 encs.push(encodings.encodingRaw);
1235
1236 // Psuedo-encoding settings
1237 encs.push(encodings.pseudoEncodingQualityLevel0 + 6);
1238 encs.push(encodings.pseudoEncodingCompressLevel0 + 2);
1239
1240 encs.push(encodings.pseudoEncodingDesktopSize);
1241 encs.push(encodings.pseudoEncodingLastRect);
1242 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
1243 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
1244 encs.push(encodings.pseudoEncodingXvp);
1245 encs.push(encodings.pseudoEncodingFence);
1246 encs.push(encodings.pseudoEncodingContinuousUpdates);
1247
1248 if (this._fb_depth == 24) {
1249 encs.push(encodings.pseudoEncodingCursor);
1250 }
1251
1252 RFB.messages.clientEncodings(this._sock, encs);
1253 }
1254
1255 /* RFB protocol initialization states:
1256 * ProtocolVersion
1257 * Security
1258 * Authentication
1259 * SecurityResult
1260 * ClientInitialization - not triggered by server message
1261 * ServerInitialization
1262 */
1263 _init_msg() {
1264 switch (this._rfb_init_state) {
1265 case 'ProtocolVersion':
1266 return this._negotiate_protocol_version();
1267
1268 case 'Security':
1269 return this._negotiate_security();
1270
1271 case 'Authentication':
1272 return this._negotiate_authentication();
1273
1274 case 'SecurityResult':
1275 return this._handle_security_result();
1276
1277 case 'SecurityReason':
1278 return this._handle_security_reason();
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 passwordChars = password.split('').map(c => c.charCodeAt(0));
1722 return (new DES(passwordChars)).encrypt(challenge);
1723 }
1724 }
1725
1726 // Class Methods
1727 RFB.messages = {
1728 keyEvent(sock, keysym, down) {
1729 const buff = sock._sQ;
1730 const offset = sock._sQlen;
1731
1732 buff[offset] = 4; // msg-type
1733 buff[offset + 1] = down;
1734
1735 buff[offset + 2] = 0;
1736 buff[offset + 3] = 0;
1737
1738 buff[offset + 4] = (keysym >> 24);
1739 buff[offset + 5] = (keysym >> 16);
1740 buff[offset + 6] = (keysym >> 8);
1741 buff[offset + 7] = keysym;
1742
1743 sock._sQlen += 8;
1744 sock.flush();
1745 },
1746
1747 QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
1748 function getRFBkeycode(xt_scancode) {
1749 const upperByte = (keycode >> 8);
1750 const lowerByte = (keycode & 0x00ff);
1751 if (upperByte === 0xe0 && lowerByte < 0x7f) {
1752 return lowerByte | 0x80;
1753 }
1754 return xt_scancode;
1755 }
1756
1757 const buff = sock._sQ;
1758 const offset = sock._sQlen;
1759
1760 buff[offset] = 255; // msg-type
1761 buff[offset + 1] = 0; // sub msg-type
1762
1763 buff[offset + 2] = (down >> 8);
1764 buff[offset + 3] = down;
1765
1766 buff[offset + 4] = (keysym >> 24);
1767 buff[offset + 5] = (keysym >> 16);
1768 buff[offset + 6] = (keysym >> 8);
1769 buff[offset + 7] = keysym;
1770
1771 const RFBkeycode = getRFBkeycode(keycode);
1772
1773 buff[offset + 8] = (RFBkeycode >> 24);
1774 buff[offset + 9] = (RFBkeycode >> 16);
1775 buff[offset + 10] = (RFBkeycode >> 8);
1776 buff[offset + 11] = RFBkeycode;
1777
1778 sock._sQlen += 12;
1779 sock.flush();
1780 },
1781
1782 pointerEvent(sock, x, y, mask) {
1783 const buff = sock._sQ;
1784 const offset = sock._sQlen;
1785
1786 buff[offset] = 5; // msg-type
1787
1788 buff[offset + 1] = mask;
1789
1790 buff[offset + 2] = x >> 8;
1791 buff[offset + 3] = x;
1792
1793 buff[offset + 4] = y >> 8;
1794 buff[offset + 5] = y;
1795
1796 sock._sQlen += 6;
1797 sock.flush();
1798 },
1799
1800 // TODO(directxman12): make this unicode compatible?
1801 clientCutText(sock, text) {
1802 const buff = sock._sQ;
1803 const offset = sock._sQlen;
1804
1805 buff[offset] = 6; // msg-type
1806
1807 buff[offset + 1] = 0; // padding
1808 buff[offset + 2] = 0; // padding
1809 buff[offset + 3] = 0; // padding
1810
1811 let length = text.length;
1812
1813 buff[offset + 4] = length >> 24;
1814 buff[offset + 5] = length >> 16;
1815 buff[offset + 6] = length >> 8;
1816 buff[offset + 7] = length;
1817
1818 sock._sQlen += 8;
1819
1820 // We have to keep track of from where in the text we begin creating the
1821 // buffer for the flush in the next iteration.
1822 let textOffset = 0;
1823
1824 let remaining = length;
1825 while (remaining > 0) {
1826
1827 let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
1828 for (let i = 0; i < flushSize; i++) {
1829 buff[sock._sQlen + i] = text.charCodeAt(textOffset + i);
1830 }
1831
1832 sock._sQlen += flushSize;
1833 sock.flush();
1834
1835 remaining -= flushSize;
1836 textOffset += flushSize;
1837 }
1838 },
1839
1840 setDesktopSize(sock, width, height, id, flags) {
1841 const buff = sock._sQ;
1842 const offset = sock._sQlen;
1843
1844 buff[offset] = 251; // msg-type
1845 buff[offset + 1] = 0; // padding
1846 buff[offset + 2] = width >> 8; // width
1847 buff[offset + 3] = width;
1848 buff[offset + 4] = height >> 8; // height
1849 buff[offset + 5] = height;
1850
1851 buff[offset + 6] = 1; // number-of-screens
1852 buff[offset + 7] = 0; // padding
1853
1854 // screen array
1855 buff[offset + 8] = id >> 24; // id
1856 buff[offset + 9] = id >> 16;
1857 buff[offset + 10] = id >> 8;
1858 buff[offset + 11] = id;
1859 buff[offset + 12] = 0; // x-position
1860 buff[offset + 13] = 0;
1861 buff[offset + 14] = 0; // y-position
1862 buff[offset + 15] = 0;
1863 buff[offset + 16] = width >> 8; // width
1864 buff[offset + 17] = width;
1865 buff[offset + 18] = height >> 8; // height
1866 buff[offset + 19] = height;
1867 buff[offset + 20] = flags >> 24; // flags
1868 buff[offset + 21] = flags >> 16;
1869 buff[offset + 22] = flags >> 8;
1870 buff[offset + 23] = flags;
1871
1872 sock._sQlen += 24;
1873 sock.flush();
1874 },
1875
1876 clientFence(sock, flags, payload) {
1877 const buff = sock._sQ;
1878 const offset = sock._sQlen;
1879
1880 buff[offset] = 248; // msg-type
1881
1882 buff[offset + 1] = 0; // padding
1883 buff[offset + 2] = 0; // padding
1884 buff[offset + 3] = 0; // padding
1885
1886 buff[offset + 4] = flags >> 24; // flags
1887 buff[offset + 5] = flags >> 16;
1888 buff[offset + 6] = flags >> 8;
1889 buff[offset + 7] = flags;
1890
1891 const n = payload.length;
1892
1893 buff[offset + 8] = n; // length
1894
1895 for (let i = 0; i < n; i++) {
1896 buff[offset + 9 + i] = payload.charCodeAt(i);
1897 }
1898
1899 sock._sQlen += 9 + n;
1900 sock.flush();
1901 },
1902
1903 enableContinuousUpdates(sock, enable, x, y, width, height) {
1904 const buff = sock._sQ;
1905 const offset = sock._sQlen;
1906
1907 buff[offset] = 150; // msg-type
1908 buff[offset + 1] = enable; // enable-flag
1909
1910 buff[offset + 2] = x >> 8; // x
1911 buff[offset + 3] = x;
1912 buff[offset + 4] = y >> 8; // y
1913 buff[offset + 5] = y;
1914 buff[offset + 6] = width >> 8; // width
1915 buff[offset + 7] = width;
1916 buff[offset + 8] = height >> 8; // height
1917 buff[offset + 9] = height;
1918
1919 sock._sQlen += 10;
1920 sock.flush();
1921 },
1922
1923 pixelFormat(sock, depth, true_color) {
1924 const buff = sock._sQ;
1925 const offset = sock._sQlen;
1926
1927 let bpp;
1928
1929 if (depth > 16) {
1930 bpp = 32;
1931 } else if (depth > 8) {
1932 bpp = 16;
1933 } else {
1934 bpp = 8;
1935 }
1936
1937 const bits = Math.floor(depth/3);
1938
1939 buff[offset] = 0; // msg-type
1940
1941 buff[offset + 1] = 0; // padding
1942 buff[offset + 2] = 0; // padding
1943 buff[offset + 3] = 0; // padding
1944
1945 buff[offset + 4] = bpp; // bits-per-pixel
1946 buff[offset + 5] = depth; // depth
1947 buff[offset + 6] = 0; // little-endian
1948 buff[offset + 7] = true_color ? 1 : 0; // true-color
1949
1950 buff[offset + 8] = 0; // red-max
1951 buff[offset + 9] = (1 << bits) - 1; // red-max
1952
1953 buff[offset + 10] = 0; // green-max
1954 buff[offset + 11] = (1 << bits) - 1; // green-max
1955
1956 buff[offset + 12] = 0; // blue-max
1957 buff[offset + 13] = (1 << bits) - 1; // blue-max
1958
1959 buff[offset + 14] = bits * 2; // red-shift
1960 buff[offset + 15] = bits * 1; // green-shift
1961 buff[offset + 16] = bits * 0; // blue-shift
1962
1963 buff[offset + 17] = 0; // padding
1964 buff[offset + 18] = 0; // padding
1965 buff[offset + 19] = 0; // padding
1966
1967 sock._sQlen += 20;
1968 sock.flush();
1969 },
1970
1971 clientEncodings(sock, encodings) {
1972 const buff = sock._sQ;
1973 const offset = sock._sQlen;
1974
1975 buff[offset] = 2; // msg-type
1976 buff[offset + 1] = 0; // padding
1977
1978 buff[offset + 2] = encodings.length >> 8;
1979 buff[offset + 3] = encodings.length;
1980
1981 let j = offset + 4;
1982 for (let i = 0; i < encodings.length; i++) {
1983 const enc = encodings[i];
1984 buff[j] = enc >> 24;
1985 buff[j + 1] = enc >> 16;
1986 buff[j + 2] = enc >> 8;
1987 buff[j + 3] = enc;
1988
1989 j += 4;
1990 }
1991
1992 sock._sQlen += j - offset;
1993 sock.flush();
1994 },
1995
1996 fbUpdateRequest(sock, incremental, x, y, w, h) {
1997 const buff = sock._sQ;
1998 const offset = sock._sQlen;
1999
2000 if (typeof(x) === "undefined") { x = 0; }
2001 if (typeof(y) === "undefined") { y = 0; }
2002
2003 buff[offset] = 3; // msg-type
2004 buff[offset + 1] = incremental ? 1 : 0;
2005
2006 buff[offset + 2] = (x >> 8) & 0xFF;
2007 buff[offset + 3] = x & 0xFF;
2008
2009 buff[offset + 4] = (y >> 8) & 0xFF;
2010 buff[offset + 5] = y & 0xFF;
2011
2012 buff[offset + 6] = (w >> 8) & 0xFF;
2013 buff[offset + 7] = w & 0xFF;
2014
2015 buff[offset + 8] = (h >> 8) & 0xFF;
2016 buff[offset + 9] = h & 0xFF;
2017
2018 sock._sQlen += 10;
2019 sock.flush();
2020 },
2021
2022 xvpOp(sock, ver, op) {
2023 const buff = sock._sQ;
2024 const offset = sock._sQlen;
2025
2026 buff[offset] = 250; // msg-type
2027 buff[offset + 1] = 0; // padding
2028
2029 buff[offset + 2] = ver;
2030 buff[offset + 3] = op;
2031
2032 sock._sQlen += 4;
2033 sock.flush();
2034 }
2035 };
2036
2037 RFB.cursors = {
2038 none: {
2039 rgbaPixels: new Uint8Array(),
2040 w: 0, h: 0,
2041 hotx: 0, hoty: 0,
2042 },
2043
2044 dot: {
2045 /* eslint-disable indent */
2046 rgbaPixels: new Uint8Array([
2047 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2048 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
2049 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2050 ]),
2051 /* eslint-enable indent */
2052 w: 3, h: 3,
2053 hotx: 1, hoty: 1,
2054 }
2055 };