2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2020 The noVNC Authors
4 * Licensed under MPL 2.0 (see LICENSE.txt)
6 * See README.md for usage and integration instructions.
10import { toUnsigned32bit, toSigned32bit } from './util/int.js';
11import * as Log from './util/logging.js';
12import { encodeUTF8, decodeUTF8 } from './util/strings.js';
13import { dragThreshold } from './util/browser.js';
14import { clientToElement } from './util/element.js';
15import { setCapture } from './util/events.js';
16import EventTargetMixin from './util/eventtarget.js';
17import Display from "./display.js";
18import Inflator from "./inflator.js";
19import Deflator from "./deflator.js";
20import Keyboard from "./input/keyboard.js";
21import GestureHandler from "./input/gesturehandler.js";
22import Cursor from "./util/cursor.js";
23import Websock from "./websock.js";
24import KeyTable from "./input/keysym.js";
25import XtScancode from "./input/xtscancodes.js";
26import { encodings } from "./encodings.js";
27import RSAAESAuthenticationState from "./ra2.js";
28import legacyCrypto from "./crypto/crypto.js";
30import RawDecoder from "./decoders/raw.js";
31import CopyRectDecoder from "./decoders/copyrect.js";
32import RREDecoder from "./decoders/rre.js";
33import HextileDecoder from "./decoders/hextile.js";
34import TightDecoder from "./decoders/tight.js";
35import TightPNGDecoder from "./decoders/tightpng.js";
36import ZRLEDecoder from "./decoders/zrle.js";
37import JPEGDecoder from "./decoders/jpeg.js";
39// How many seconds to wait for a disconnect to finish
40const DISCONNECT_TIMEOUT = 3;
41const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
43// Minimum wait (ms) between two mouse moves
44const MOUSE_MOVE_DELAY = 17;
47const WHEEL_STEP = 50; // Pixels needed for one step
48const WHEEL_LINE_HEIGHT = 19; // Assumed pixels for one line step
51const GESTURE_ZOOMSENS = 75;
52const GESTURE_SCRLSENS = 50;
53const DOUBLE_TAP_TIMEOUT = 1000;
54const DOUBLE_TAP_THRESHOLD = 50;
57const securityTypeNone = 1;
58const securityTypeVNCAuth = 2;
59const securityTypeRA2ne = 6;
60const securityTypeTight = 16;
61const securityTypeVeNCrypt = 19;
62const securityTypeXVP = 22;
63const securityTypeARD = 30;
64const securityTypeMSLogonII = 113;
66// Special Tight security types
67const securityTypeUnixLogon = 129;
69// VeNCrypt security types
70const securityTypePlain = 256;
72// Extended clipboard pseudo-encoding formats
73const extendedClipboardFormatText = 1;
74/*eslint-disable no-unused-vars */
75const extendedClipboardFormatRtf = 1 << 1;
76const extendedClipboardFormatHtml = 1 << 2;
77const extendedClipboardFormatDib = 1 << 3;
78const extendedClipboardFormatFiles = 1 << 4;
81// Extended clipboard pseudo-encoding actions
82const extendedClipboardActionCaps = 1 << 24;
83const extendedClipboardActionRequest = 1 << 25;
84const extendedClipboardActionPeek = 1 << 26;
85const extendedClipboardActionNotify = 1 << 27;
86const extendedClipboardActionProvide = 1 << 28;
88export default class RFB extends EventTargetMixin {
89 constructor(target, urlOrChannel, options) {
91 throw new Error("Must specify target");
94 throw new Error("Must specify URL, WebSocket or RTCDataChannel");
97 // We rely on modern APIs which might not be available in an
99 if (!window.isSecureContext) {
100 Log.Error("noVNC requires a secure context (TLS). Expect crashes!");
105 this._target = target;
107 if (typeof urlOrChannel === "string") {
108 this._url = urlOrChannel;
111 this._rawChannel = urlOrChannel;
114 // Connection details
115 options = options || {};
116 this._rfbCredentials = options.credentials || {};
117 this._shared = 'shared' in options ? !!options.shared : true;
118 this._repeaterID = options.repeaterID || '';
119 this._wsProtocols = options.wsProtocols || [];
122 this._rfbConnectionState = '';
123 this._rfbInitState = '';
124 this._rfbAuthScheme = -1;
125 this._rfbCleanDisconnect = true;
126 this._rfbRSAAESAuthenticationState = null;
128 // Server capabilities
129 this._rfbVersion = 0;
130 this._rfbMaxVersion = 3.8;
131 this._rfbTightVNC = false;
132 this._rfbVeNCryptState = 0;
140 this._capabilities = { power: false };
142 this._supportsFence = false;
144 this._supportsContinuousUpdates = false;
145 this._enabledContinuousUpdates = false;
147 this._supportsSetDesktopSize = false;
149 this._screenFlags = 0;
151 this._qemuExtKeyEventSupported = false;
153 this._clipboardText = null;
154 this._clipboardServerCapabilitiesActions = {};
155 this._clipboardServerCapabilitiesFormats = {};
158 this._sock = null; // Websock object
159 this._display = null; // Display object
160 this._flushing = false; // Display flushing state
161 this._keyboard = null; // Keyboard input handler object
162 this._gestures = null; // Gesture input handler object
163 this._resizeObserver = null; // Resize observer object
166 this._disconnTimer = null; // disconnection timer
167 this._resizeTimeout = null; // resize rate limiting
168 this._mouseMoveTimer = null;
184 this._mouseButtonMask = 0;
185 this._mouseLastMoveTime = 0;
186 this._viewportDragging = false;
187 this._viewportDragPos = {};
188 this._viewportHasMoved = false;
189 this._accumulatedWheelDeltaX = 0;
190 this._accumulatedWheelDeltaY = 0;
193 this._gestureLastTapTime = null;
194 this._gestureFirstDoubleTapEv = null;
195 this._gestureLastMagnitudeX = 0;
196 this._gestureLastMagnitudeY = 0;
198 // Bound event handlers
199 this._eventHandlers = {
200 focusCanvas: this._focusCanvas.bind(this),
201 handleResize: this._handleResize.bind(this),
202 handleMouse: this._handleMouse.bind(this),
203 handleWheel: this._handleWheel.bind(this),
204 handleGesture: this._handleGesture.bind(this),
205 handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this),
206 handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this),
210 Log.Debug(">> RFB.constructor");
212 // Create DOM elements
213 this._screen = document.createElement('div');
214 this._screen.style.display = 'flex';
215 this._screen.style.width = '100%';
216 this._screen.style.height = '100%';
217 this._screen.style.overflow = 'auto';
218 this._screen.style.background = DEFAULT_BACKGROUND;
219 this._canvas = document.createElement('canvas');
220 this._canvas.style.margin = 'auto';
221 // Some browsers add an outline on focus
222 this._canvas.style.outline = 'none';
223 this._canvas.width = 0;
224 this._canvas.height = 0;
225 this._canvas.tabIndex = -1;
226 this._screen.appendChild(this._canvas);
229 this._cursor = new Cursor();
231 // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
232 // it. Result: no cursor at all until a window border or an edit field
233 // is hit blindly. But there are also VNC servers that draw the cursor
234 // in the framebuffer and don't send the empty local cursor. There is
235 // no way to satisfy both sides.
237 // The spec is unclear on this "initial cursor" issue. Many other
238 // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
239 // initial cursor instead.
240 this._cursorImage = RFB.cursors.none;
242 // populate decoder array with objects
243 this._decoders[encodings.encodingRaw] = new RawDecoder();
244 this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
245 this._decoders[encodings.encodingRRE] = new RREDecoder();
246 this._decoders[encodings.encodingHextile] = new HextileDecoder();
247 this._decoders[encodings.encodingTight] = new TightDecoder();
248 this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
249 this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();
250 this._decoders[encodings.encodingJPEG] = new JPEGDecoder();
252 // NB: nothing that needs explicit teardown should be done
253 // before this point, since this can throw an exception
255 this._display = new Display(this._canvas);
257 Log.Error("Display exception: " + exc);
261 this._keyboard = new Keyboard(this._canvas);
262 this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
263 this._remoteCapsLock = null; // Null indicates unknown or irrelevant
264 this._remoteNumLock = null;
266 this._gestures = new GestureHandler();
268 this._sock = new Websock();
269 this._sock.on('open', this._socketOpen.bind(this));
270 this._sock.on('close', this._socketClose.bind(this));
271 this._sock.on('message', this._handleMessage.bind(this));
272 this._sock.on('error', this._socketError.bind(this));
274 this._expectedClientWidth = null;
275 this._expectedClientHeight = null;
276 this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize);
278 // All prepared, kick off the connection
279 this._updateConnectionState('connecting');
281 Log.Debug("<< RFB.constructor");
283 // ===== PROPERTIES =====
285 this.dragViewport = false;
286 this.focusOnClick = true;
288 this._viewOnly = false;
289 this._clipViewport = false;
290 this._clippingViewport = false;
291 this._scaleViewport = false;
292 this._resizeSession = false;
294 this._showDotCursor = false;
295 if (options.showDotCursor !== undefined) {
296 Log.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated");
297 this._showDotCursor = options.showDotCursor;
300 this._qualityLevel = 6;
301 this._compressionLevel = 2;
304 // ===== PROPERTIES =====
306 get viewOnly() { return this._viewOnly; }
307 set viewOnly(viewOnly) {
308 this._viewOnly = viewOnly;
310 if (this._rfbConnectionState === "connecting" ||
311 this._rfbConnectionState === "connected") {
313 this._keyboard.ungrab();
315 this._keyboard.grab();
320 get capabilities() { return this._capabilities; }
322 get clippingViewport() { return this._clippingViewport; }
323 _setClippingViewport(on) {
324 if (on === this._clippingViewport) {
327 this._clippingViewport = on;
328 this.dispatchEvent(new CustomEvent("clippingviewport",
329 { detail: this._clippingViewport }));
332 get touchButton() { return 0; }
333 set touchButton(button) { Log.Warn("Using old API!"); }
335 get clipViewport() { return this._clipViewport; }
336 set clipViewport(viewport) {
337 this._clipViewport = viewport;
341 get scaleViewport() { return this._scaleViewport; }
342 set scaleViewport(scale) {
343 this._scaleViewport = scale;
344 // Scaling trumps clipping, so we may need to adjust
345 // clipping when enabling or disabling scaling
346 if (scale && this._clipViewport) {
350 if (!scale && this._clipViewport) {
355 get resizeSession() { return this._resizeSession; }
356 set resizeSession(resize) {
357 this._resizeSession = resize;
359 this._requestRemoteResize();
363 get showDotCursor() { return this._showDotCursor; }
364 set showDotCursor(show) {
365 this._showDotCursor = show;
366 this._refreshCursor();
369 get background() { return this._screen.style.background; }
370 set background(cssValue) { this._screen.style.background = cssValue; }
373 return this._qualityLevel;
375 set qualityLevel(qualityLevel) {
376 if (!Number.isInteger(qualityLevel) || qualityLevel < 0 || qualityLevel > 9) {
377 Log.Error("qualityLevel must be an integer between 0 and 9");
381 if (this._qualityLevel === qualityLevel) {
385 this._qualityLevel = qualityLevel;
387 if (this._rfbConnectionState === 'connected') {
388 this._sendEncodings();
392 get compressionLevel() {
393 return this._compressionLevel;
395 set compressionLevel(compressionLevel) {
396 if (!Number.isInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) {
397 Log.Error("compressionLevel must be an integer between 0 and 9");
401 if (this._compressionLevel === compressionLevel) {
405 this._compressionLevel = compressionLevel;
407 if (this._rfbConnectionState === 'connected') {
408 this._sendEncodings();
412 // ===== PUBLIC METHODS =====
415 this._updateConnectionState('disconnecting');
416 this._sock.off('error');
417 this._sock.off('message');
418 this._sock.off('open');
419 if (this._rfbRSAAESAuthenticationState !== null) {
420 this._rfbRSAAESAuthenticationState.disconnect();
425 if (this._rfbRSAAESAuthenticationState !== null) {
426 this._rfbRSAAESAuthenticationState.approveServer();
430 sendCredentials(creds) {
431 this._rfbCredentials = creds;
432 this._resumeAuthentication();
436 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
437 Log.Info("Sending Ctrl-Alt-Del");
439 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
440 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
441 this.sendKey(KeyTable.XK_Delete, "Delete", true);
442 this.sendKey(KeyTable.XK_Delete, "Delete", false);
443 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
444 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
459 // Send a key press. If 'down' is not specified then send a down key
460 // followed by an up key.
461 sendKey(keysym, code, down) {
462 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
464 if (down === undefined) {
465 this.sendKey(keysym, code, true);
466 this.sendKey(keysym, code, false);
470 const scancode = XtScancode[code];
472 if (this._qemuExtKeyEventSupported && scancode) {
474 keysym = keysym || 0;
476 Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
478 RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
483 Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
484 RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
489 this._canvas.focus(options);
496 clipboardPasteFrom(text) {
497 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
499 if (this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] &&
500 this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
502 this._clipboardText = text;
503 RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
509 // eslint-disable-next-line no-unused-vars
510 for (let codePoint of text) {
514 data = new Uint8Array(length);
517 for (let codePoint of text) {
518 let code = codePoint.codePointAt(0);
520 /* Only ISO 8859-1 is supported */
528 RFB.messages.clientCutText(this._sock, data);
533 return this._display.getImageData();
536 toDataURL(type, encoderOptions) {
537 return this._display.toDataURL(type, encoderOptions);
540 toBlob(callback, type, quality) {
541 return this._display.toBlob(callback, type, quality);
544 // ===== PRIVATE METHODS =====
547 Log.Debug(">> RFB.connect");
550 Log.Info(`connecting to ${this._url}`);
551 this._sock.open(this._url, this._wsProtocols);
553 Log.Info(`attaching ${this._rawChannel} to Websock`);
554 this._sock.attach(this._rawChannel);
556 if (this._sock.readyState === 'closed') {
557 throw Error("Cannot use already closed WebSocket/RTCDataChannel");
560 if (this._sock.readyState === 'open') {
561 // FIXME: _socketOpen() can in theory call _fail(), which
562 // isn't allowed this early, but I'm not sure that can
563 // happen without a bug messing up our state variables
568 // Make our elements part of the page
569 this._target.appendChild(this._screen);
571 this._gestures.attach(this._canvas);
573 this._cursor.attach(this._canvas);
574 this._refreshCursor();
576 // Monitor size changes of the screen element
577 this._resizeObserver.observe(this._screen);
579 // Always grab focus on some kind of click event
580 this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
581 this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);
584 this._canvas.addEventListener('mousedown', this._eventHandlers.handleMouse);
585 this._canvas.addEventListener('mouseup', this._eventHandlers.handleMouse);
586 this._canvas.addEventListener('mousemove', this._eventHandlers.handleMouse);
587 // Prevent middle-click pasting (see handler for why we bind to document)
588 this._canvas.addEventListener('click', this._eventHandlers.handleMouse);
589 // preventDefault() on mousedown doesn't stop this event for some
590 // reason so we have to explicitly block it
591 this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);
594 this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);
597 this._canvas.addEventListener("gesturestart", this._eventHandlers.handleGesture);
598 this._canvas.addEventListener("gesturemove", this._eventHandlers.handleGesture);
599 this._canvas.addEventListener("gestureend", this._eventHandlers.handleGesture);
601 Log.Debug("<< RFB.connect");
605 Log.Debug(">> RFB.disconnect");
606 this._cursor.detach();
607 this._canvas.removeEventListener("gesturestart", this._eventHandlers.handleGesture);
608 this._canvas.removeEventListener("gesturemove", this._eventHandlers.handleGesture);
609 this._canvas.removeEventListener("gestureend", this._eventHandlers.handleGesture);
610 this._canvas.removeEventListener("wheel", this._eventHandlers.handleWheel);
611 this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse);
612 this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse);
613 this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);
614 this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);
615 this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);
616 this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
617 this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
618 this._resizeObserver.disconnect();
619 this._keyboard.ungrab();
620 this._gestures.detach();
623 this._target.removeChild(this._screen);
625 if (e.name === 'NotFoundError') {
626 // Some cases where the initial connection fails
627 // can disconnect before the _screen is created
632 clearTimeout(this._resizeTimeout);
633 clearTimeout(this._mouseMoveTimer);
634 Log.Debug("<< RFB.disconnect");
638 if ((this._rfbConnectionState === 'connecting') &&
639 (this._rfbInitState === '')) {
640 this._rfbInitState = 'ProtocolVersion';
641 Log.Debug("Starting VNC handshake");
643 this._fail("Unexpected server connection while " +
644 this._rfbConnectionState);
649 Log.Debug("WebSocket on-close event");
652 msg = "(code: " + e.code;
654 msg += ", reason: " + e.reason;
658 switch (this._rfbConnectionState) {
660 this._fail("Connection closed " + msg);
663 // Handle disconnects that were initiated server-side
664 this._updateConnectionState('disconnecting');
665 this._updateConnectionState('disconnected');
667 case 'disconnecting':
668 // Normal disconnection path
669 this._updateConnectionState('disconnected');
672 this._fail("Unexpected server disconnect " +
673 "when already disconnected " + msg);
676 this._fail("Unexpected server disconnect before connecting " +
680 this._sock.off('close');
681 // Delete reference to raw channel to allow cleanup.
682 this._rawChannel = null;
686 Log.Warn("WebSocket on-error event");
689 _focusCanvas(event) {
690 if (!this.focusOnClick) {
694 this.focus({ preventScroll: true });
697 _setDesktopName(name) {
699 this.dispatchEvent(new CustomEvent(
701 { detail: { name: this._fbName } }));
704 _saveExpectedClientSize() {
705 this._expectedClientWidth = this._screen.clientWidth;
706 this._expectedClientHeight = this._screen.clientHeight;
709 _currentClientSize() {
710 return [this._screen.clientWidth, this._screen.clientHeight];
713 _clientHasExpectedSize() {
714 const [currentWidth, currentHeight] = this._currentClientSize();
715 return currentWidth == this._expectedClientWidth &&
716 currentHeight == this._expectedClientHeight;
720 // Don't change anything if the client size is already as expected
721 if (this._clientHasExpectedSize()) {
724 // If the window resized then our screen element might have
725 // as well. Update the viewport dimensions.
726 window.requestAnimationFrame(() => {
731 if (this._resizeSession) {
732 // Request changing the resolution of the remote display to
733 // the size of the local browser viewport.
735 // In order to not send multiple requests before the browser-resize
736 // is finished we wait 0.5 seconds before sending the request.
737 clearTimeout(this._resizeTimeout);
738 this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500);
742 // Update state of clipping in Display object, and make sure the
743 // configured viewport matches the current screen size
745 const curClip = this._display.clipViewport;
746 let newClip = this._clipViewport;
748 if (this._scaleViewport) {
749 // Disable viewport clipping if we are scaling
753 if (curClip !== newClip) {
754 this._display.clipViewport = newClip;
758 // When clipping is enabled, the screen is limited to
759 // the size of the container.
760 const size = this._screenSize();
761 this._display.viewportChangeSize(size.w, size.h);
762 this._fixScrollbars();
763 this._setClippingViewport(size.w < this._display.width ||
764 size.h < this._display.height);
766 this._setClippingViewport(false);
769 // When changing clipping we might show or hide scrollbars.
770 // This causes the expected client dimensions to change.
771 if (curClip !== newClip) {
772 this._saveExpectedClientSize();
777 if (!this._scaleViewport) {
778 this._display.scale = 1.0;
780 const size = this._screenSize();
781 this._display.autoscale(size.w, size.h);
783 this._fixScrollbars();
786 // Requests a change of remote desktop size. This message is an extension
787 // and may only be sent if we have received an ExtendedDesktopSize message
788 _requestRemoteResize() {
789 clearTimeout(this._resizeTimeout);
790 this._resizeTimeout = null;
792 if (!this._resizeSession || this._viewOnly ||
793 !this._supportsSetDesktopSize) {
797 const size = this._screenSize();
799 RFB.messages.setDesktopSize(this._sock,
800 Math.floor(size.w), Math.floor(size.h),
801 this._screenID, this._screenFlags);
803 Log.Debug('Requested new desktop size: ' +
804 size.w + 'x' + size.h);
807 // Gets the the size of the available screen
809 let r = this._screen.getBoundingClientRect();
810 return { w: r.width, h: r.height };
814 // This is a hack because Safari on macOS screws up the calculation
815 // for when scrollbars are needed. We get scrollbars when making the
816 // browser smaller, despite remote resize being enabled. So to fix it
817 // we temporarily toggle them off and on.
818 const orig = this._screen.style.overflow;
819 this._screen.style.overflow = 'hidden';
820 // Force Safari to recalculate the layout by asking for
821 // an element's dimensions
822 this._screen.getBoundingClientRect();
823 this._screen.style.overflow = orig;
831 * disconnected - permanent state
833 _updateConnectionState(state) {
834 const oldstate = this._rfbConnectionState;
836 if (state === oldstate) {
837 Log.Debug("Already in state '" + state + "', ignoring");
841 // The 'disconnected' state is permanent for each RFB object
842 if (oldstate === 'disconnected') {
843 Log.Error("Tried changing state of a disconnected RFB object");
847 // Ensure proper transitions before doing anything
850 if (oldstate !== 'connecting') {
851 Log.Error("Bad transition to connected state, " +
852 "previous connection state: " + oldstate);
858 if (oldstate !== 'disconnecting') {
859 Log.Error("Bad transition to disconnected state, " +
860 "previous connection state: " + oldstate);
866 if (oldstate !== '') {
867 Log.Error("Bad transition to connecting state, " +
868 "previous connection state: " + oldstate);
873 case 'disconnecting':
874 if (oldstate !== 'connected' && oldstate !== 'connecting') {
875 Log.Error("Bad transition to disconnecting state, " +
876 "previous connection state: " + oldstate);
882 Log.Error("Unknown connection state: " + state);
886 // State change actions
888 this._rfbConnectionState = state;
890 Log.Debug("New state '" + state + "', was '" + oldstate + "'.");
892 if (this._disconnTimer && state !== 'disconnecting') {
893 Log.Debug("Clearing disconnect timer");
894 clearTimeout(this._disconnTimer);
895 this._disconnTimer = null;
897 // make sure we don't get a double event
898 this._sock.off('close');
907 this.dispatchEvent(new CustomEvent("connect", { detail: {} }));
910 case 'disconnecting':
913 this._disconnTimer = setTimeout(() => {
914 Log.Error("Disconnection timed out.");
915 this._updateConnectionState('disconnected');
916 }, DISCONNECT_TIMEOUT * 1000);
920 this.dispatchEvent(new CustomEvent(
921 "disconnect", { detail:
922 { clean: this._rfbCleanDisconnect } }));
927 /* Print errors and disconnect
929 * The parameter 'details' is used for information that
930 * should be logged but not sent to the user interface.
933 switch (this._rfbConnectionState) {
934 case 'disconnecting':
935 Log.Error("Failed when disconnecting: " + details);
938 Log.Error("Failed while connected: " + details);
941 Log.Error("Failed when connecting: " + details);
944 Log.Error("RFB failure: " + details);
947 this._rfbCleanDisconnect = false; //This is sent to the UI
949 // Transition to disconnected without waiting for socket to close
950 this._updateConnectionState('disconnecting');
951 this._updateConnectionState('disconnected');
956 _setCapability(cap, val) {
957 this._capabilities[cap] = val;
958 this.dispatchEvent(new CustomEvent("capabilities",
959 { detail: { capabilities: this._capabilities } }));
963 if (this._sock.rQwait("message", 1)) {
964 Log.Warn("handleMessage called on an empty receive queue");
968 switch (this._rfbConnectionState) {
970 Log.Error("Got data while disconnected");
974 if (this._flushing) {
977 if (!this._normalMsg()) {
980 if (this._sock.rQwait("message", 1)) {
986 while (this._rfbConnectionState === 'connecting') {
987 if (!this._initMsg()) {
993 Log.Error("Got data while in an invalid state");
998 _handleKeyEvent(keysym, code, down, numlock, capslock) {
999 // If remote state of capslock is known, and it doesn't match the local led state of
1000 // the keyboard, we send a capslock keypress first to bring it into sync.
1001 // If we just pressed CapsLock, or we toggled it remotely due to it being out of sync
1002 // we clear the remote state so that we don't send duplicate or spurious fixes,
1003 // since it may take some time to receive the new remote CapsLock state.
1004 if (code == 'CapsLock' && down) {
1005 this._remoteCapsLock = null;
1007 if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) {
1008 Log.Debug("Fixing remote caps lock");
1010 this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true);
1011 this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false);
1012 // We clear the remote capsLock state when we do this to prevent issues with doing this twice
1013 // before we receive an update of the the remote state.
1014 this._remoteCapsLock = null;
1017 // Logic for numlock is exactly the same.
1018 if (code == 'NumLock' && down) {
1019 this._remoteNumLock = null;
1021 if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) {
1022 Log.Debug("Fixing remote num lock");
1023 this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true);
1024 this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false);
1025 this._remoteNumLock = null;
1027 this.sendKey(keysym, code, down);
1032 * We don't check connection status or viewOnly here as the
1033 * mouse events might be used to control the viewport
1036 if (ev.type === 'click') {
1038 * Note: This is only needed for the 'click' event as it fails
1039 * to fire properly for the target element so we have
1040 * to listen on the document element instead.
1042 if (ev.target !== this._canvas) {
1047 // FIXME: if we're in view-only and not dragging,
1048 // should we stop events?
1049 ev.stopPropagation();
1050 ev.preventDefault();
1052 if ((ev.type === 'click') || (ev.type === 'contextmenu')) {
1056 let pos = clientToElement(ev.clientX, ev.clientY,
1061 setCapture(this._canvas);
1062 this._handleMouseButton(pos.x, pos.y,
1063 true, 1 << ev.button);
1066 this._handleMouseButton(pos.x, pos.y,
1067 false, 1 << ev.button);
1070 this._handleMouseMove(pos.x, pos.y);
1075 _handleMouseButton(x, y, down, bmask) {
1076 if (this.dragViewport) {
1077 if (down && !this._viewportDragging) {
1078 this._viewportDragging = true;
1079 this._viewportDragPos = {'x': x, 'y': y};
1080 this._viewportHasMoved = false;
1082 // Skip sending mouse events
1085 this._viewportDragging = false;
1087 // If we actually performed a drag then we are done
1088 // here and should not send any mouse events
1089 if (this._viewportHasMoved) {
1093 // Otherwise we treat this as a mouse click event.
1094 // Send the button down event here, as the button up
1095 // event is sent at the end of this function.
1096 this._sendMouse(x, y, bmask);
1100 // Flush waiting move event first
1101 if (this._mouseMoveTimer !== null) {
1102 clearTimeout(this._mouseMoveTimer);
1103 this._mouseMoveTimer = null;
1104 this._sendMouse(x, y, this._mouseButtonMask);
1108 this._mouseButtonMask |= bmask;
1110 this._mouseButtonMask &= ~bmask;
1113 this._sendMouse(x, y, this._mouseButtonMask);
1116 _handleMouseMove(x, y) {
1117 if (this._viewportDragging) {
1118 const deltaX = this._viewportDragPos.x - x;
1119 const deltaY = this._viewportDragPos.y - y;
1121 if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
1122 Math.abs(deltaY) > dragThreshold)) {
1123 this._viewportHasMoved = true;
1125 this._viewportDragPos = {'x': x, 'y': y};
1126 this._display.viewportChangePos(deltaX, deltaY);
1129 // Skip sending mouse events
1133 this._mousePos = { 'x': x, 'y': y };
1135 // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms
1136 if (this._mouseMoveTimer == null) {
1138 const timeSinceLastMove = Date.now() - this._mouseLastMoveTime;
1139 if (timeSinceLastMove > MOUSE_MOVE_DELAY) {
1140 this._sendMouse(x, y, this._mouseButtonMask);
1141 this._mouseLastMoveTime = Date.now();
1143 // Too soon since the latest move, wait the remaining time
1144 this._mouseMoveTimer = setTimeout(() => {
1145 this._handleDelayedMouseMove();
1146 }, MOUSE_MOVE_DELAY - timeSinceLastMove);
1151 _handleDelayedMouseMove() {
1152 this._mouseMoveTimer = null;
1153 this._sendMouse(this._mousePos.x, this._mousePos.y,
1154 this._mouseButtonMask);
1155 this._mouseLastMoveTime = Date.now();
1158 _sendMouse(x, y, mask) {
1159 if (this._rfbConnectionState !== 'connected') { return; }
1160 if (this._viewOnly) { return; } // View only, skip mouse events
1162 RFB.messages.pointerEvent(this._sock, this._display.absX(x),
1163 this._display.absY(y), mask);
1167 if (this._rfbConnectionState !== 'connected') { return; }
1168 if (this._viewOnly) { return; } // View only, skip mouse events
1170 ev.stopPropagation();
1171 ev.preventDefault();
1173 let pos = clientToElement(ev.clientX, ev.clientY,
1179 // Pixel units unless it's non-zero.
1180 // Note that if deltamode is line or page won't matter since we aren't
1181 // sending the mouse wheel delta to the server anyway.
1182 // The difference between pixel and line can be important however since
1183 // we have a threshold that can be smaller than the line height.
1184 if (ev.deltaMode !== 0) {
1185 dX *= WHEEL_LINE_HEIGHT;
1186 dY *= WHEEL_LINE_HEIGHT;
1189 // Mouse wheel events are sent in steps over VNC. This means that the VNC
1190 // protocol can't handle a wheel event with specific distance or speed.
1191 // Therefor, if we get a lot of small mouse wheel events we combine them.
1192 this._accumulatedWheelDeltaX += dX;
1193 this._accumulatedWheelDeltaY += dY;
1195 // Generate a mouse wheel step event when the accumulated delta
1196 // for one of the axes is large enough.
1197 if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) {
1198 if (this._accumulatedWheelDeltaX < 0) {
1199 this._handleMouseButton(pos.x, pos.y, true, 1 << 5);
1200 this._handleMouseButton(pos.x, pos.y, false, 1 << 5);
1201 } else if (this._accumulatedWheelDeltaX > 0) {
1202 this._handleMouseButton(pos.x, pos.y, true, 1 << 6);
1203 this._handleMouseButton(pos.x, pos.y, false, 1 << 6);
1206 this._accumulatedWheelDeltaX = 0;
1208 if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) {
1209 if (this._accumulatedWheelDeltaY < 0) {
1210 this._handleMouseButton(pos.x, pos.y, true, 1 << 3);
1211 this._handleMouseButton(pos.x, pos.y, false, 1 << 3);
1212 } else if (this._accumulatedWheelDeltaY > 0) {
1213 this._handleMouseButton(pos.x, pos.y, true, 1 << 4);
1214 this._handleMouseButton(pos.x, pos.y, false, 1 << 4);
1217 this._accumulatedWheelDeltaY = 0;
1221 _fakeMouseMove(ev, elementX, elementY) {
1222 this._handleMouseMove(elementX, elementY);
1223 this._cursor.move(ev.detail.clientX, ev.detail.clientY);
1226 _handleTapEvent(ev, bmask) {
1227 let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
1230 // If the user quickly taps multiple times we assume they meant to
1231 // hit the same spot, so slightly adjust coordinates
1233 if ((this._gestureLastTapTime !== null) &&
1234 ((Date.now() - this._gestureLastTapTime) < DOUBLE_TAP_TIMEOUT) &&
1235 (this._gestureFirstDoubleTapEv.detail.type === ev.detail.type)) {
1236 let dx = this._gestureFirstDoubleTapEv.detail.clientX - ev.detail.clientX;
1237 let dy = this._gestureFirstDoubleTapEv.detail.clientY - ev.detail.clientY;
1238 let distance = Math.hypot(dx, dy);
1240 if (distance < DOUBLE_TAP_THRESHOLD) {
1241 pos = clientToElement(this._gestureFirstDoubleTapEv.detail.clientX,
1242 this._gestureFirstDoubleTapEv.detail.clientY,
1245 this._gestureFirstDoubleTapEv = ev;
1248 this._gestureFirstDoubleTapEv = ev;
1250 this._gestureLastTapTime = Date.now();
1252 this._fakeMouseMove(this._gestureFirstDoubleTapEv, pos.x, pos.y);
1253 this._handleMouseButton(pos.x, pos.y, true, bmask);
1254 this._handleMouseButton(pos.x, pos.y, false, bmask);
1257 _handleGesture(ev) {
1260 let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
1263 case 'gesturestart':
1264 switch (ev.detail.type) {
1266 this._handleTapEvent(ev, 0x1);
1269 this._handleTapEvent(ev, 0x4);
1272 this._handleTapEvent(ev, 0x2);
1275 this._fakeMouseMove(ev, pos.x, pos.y);
1276 this._handleMouseButton(pos.x, pos.y, true, 0x1);
1279 this._fakeMouseMove(ev, pos.x, pos.y);
1280 this._handleMouseButton(pos.x, pos.y, true, 0x4);
1284 this._gestureLastMagnitudeX = ev.detail.magnitudeX;
1285 this._gestureLastMagnitudeY = ev.detail.magnitudeY;
1286 this._fakeMouseMove(ev, pos.x, pos.y);
1289 this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX,
1290 ev.detail.magnitudeY);
1291 this._fakeMouseMove(ev, pos.x, pos.y);
1297 switch (ev.detail.type) {
1304 this._fakeMouseMove(ev, pos.x, pos.y);
1307 // Always scroll in the same position.
1308 // We don't know if the mouse was moved so we need to move it
1310 this._fakeMouseMove(ev, pos.x, pos.y);
1311 while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) {
1312 this._handleMouseButton(pos.x, pos.y, true, 0x8);
1313 this._handleMouseButton(pos.x, pos.y, false, 0x8);
1314 this._gestureLastMagnitudeY += GESTURE_SCRLSENS;
1316 while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) < -GESTURE_SCRLSENS) {
1317 this._handleMouseButton(pos.x, pos.y, true, 0x10);
1318 this._handleMouseButton(pos.x, pos.y, false, 0x10);
1319 this._gestureLastMagnitudeY -= GESTURE_SCRLSENS;
1321 while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) > GESTURE_SCRLSENS) {
1322 this._handleMouseButton(pos.x, pos.y, true, 0x20);
1323 this._handleMouseButton(pos.x, pos.y, false, 0x20);
1324 this._gestureLastMagnitudeX += GESTURE_SCRLSENS;
1326 while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) < -GESTURE_SCRLSENS) {
1327 this._handleMouseButton(pos.x, pos.y, true, 0x40);
1328 this._handleMouseButton(pos.x, pos.y, false, 0x40);
1329 this._gestureLastMagnitudeX -= GESTURE_SCRLSENS;
1333 // Always scroll in the same position.
1334 // We don't know if the mouse was moved so we need to move it
1336 this._fakeMouseMove(ev, pos.x, pos.y);
1337 magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY);
1338 if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
1339 this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
1340 while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
1341 this._handleMouseButton(pos.x, pos.y, true, 0x8);
1342 this._handleMouseButton(pos.x, pos.y, false, 0x8);
1343 this._gestureLastMagnitudeX += GESTURE_ZOOMSENS;
1345 while ((magnitude - this._gestureLastMagnitudeX) < -GESTURE_ZOOMSENS) {
1346 this._handleMouseButton(pos.x, pos.y, true, 0x10);
1347 this._handleMouseButton(pos.x, pos.y, false, 0x10);
1348 this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS;
1351 this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", false);
1357 switch (ev.detail.type) {
1365 this._fakeMouseMove(ev, pos.x, pos.y);
1366 this._handleMouseButton(pos.x, pos.y, false, 0x1);
1369 this._fakeMouseMove(ev, pos.x, pos.y);
1370 this._handleMouseButton(pos.x, pos.y, false, 0x4);
1379 _negotiateProtocolVersion() {
1380 if (this._sock.rQwait("version", 12)) {
1384 const sversion = this._sock.rQshiftStr(12).substr(4, 7);
1385 Log.Info("Server ProtocolVersion: " + sversion);
1388 case "000.000": // UltraVNC repeater
1392 case "003.006": // UltraVNC
1393 this._rfbVersion = 3.3;
1396 this._rfbVersion = 3.7;
1399 case "003.889": // Apple Remote Desktop
1400 case "004.000": // Intel AMT KVM
1401 case "004.001": // RealVNC 4.6
1402 case "005.000": // RealVNC 5.3
1403 this._rfbVersion = 3.8;
1406 return this._fail("Invalid server version " + sversion);
1410 let repeaterID = "ID:" + this._repeaterID;
1411 while (repeaterID.length < 250) {
1414 this._sock.sQpushString(repeaterID);
1419 if (this._rfbVersion > this._rfbMaxVersion) {
1420 this._rfbVersion = this._rfbMaxVersion;
1423 const cversion = "00" + parseInt(this._rfbVersion, 10) +
1424 ".00" + ((this._rfbVersion * 10) % 10);
1425 this._sock.sQpushString("RFB " + cversion + "\n");
1427 Log.Debug('Sent ProtocolVersion: ' + cversion);
1429 this._rfbInitState = 'Security';
1432 _isSupportedSecurityType(type) {
1433 const clientTypes = [
1435 securityTypeVNCAuth,
1438 securityTypeVeNCrypt,
1441 securityTypeMSLogonII,
1445 return clientTypes.includes(type);
1448 _negotiateSecurity() {
1449 if (this._rfbVersion >= 3.7) {
1450 // Server sends supported list, client decides
1451 const numTypes = this._sock.rQshift8();
1452 if (this._sock.rQwait("security type", numTypes, 1)) { return false; }
1454 if (numTypes === 0) {
1455 this._rfbInitState = "SecurityReason";
1456 this._securityContext = "no security types";
1457 this._securityStatus = 1;
1461 const types = this._sock.rQshiftBytes(numTypes);
1462 Log.Debug("Server security types: " + types);
1464 // Look for a matching security type in the order that the
1466 this._rfbAuthScheme = -1;
1467 for (let type of types) {
1468 if (this._isSupportedSecurityType(type)) {
1469 this._rfbAuthScheme = type;
1474 if (this._rfbAuthScheme === -1) {
1475 return this._fail("Unsupported security types (types: " + types + ")");
1478 this._sock.sQpush8(this._rfbAuthScheme);
1482 if (this._sock.rQwait("security scheme", 4)) { return false; }
1483 this._rfbAuthScheme = this._sock.rQshift32();
1485 if (this._rfbAuthScheme == 0) {
1486 this._rfbInitState = "SecurityReason";
1487 this._securityContext = "authentication scheme";
1488 this._securityStatus = 1;
1493 this._rfbInitState = 'Authentication';
1494 Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
1499 _handleSecurityReason() {
1500 if (this._sock.rQwait("reason length", 4)) {
1503 const strlen = this._sock.rQshift32();
1507 if (this._sock.rQwait("reason", strlen, 4)) { return false; }
1508 reason = this._sock.rQshiftStr(strlen);
1511 if (reason !== "") {
1512 this.dispatchEvent(new CustomEvent(
1514 { detail: { status: this._securityStatus,
1515 reason: reason } }));
1517 return this._fail("Security negotiation failed on " +
1518 this._securityContext +
1519 " (reason: " + reason + ")");
1521 this.dispatchEvent(new CustomEvent(
1523 { detail: { status: this._securityStatus } }));
1525 return this._fail("Security negotiation failed on " +
1526 this._securityContext);
1531 _negotiateXvpAuth() {
1532 if (this._rfbCredentials.username === undefined ||
1533 this._rfbCredentials.password === undefined ||
1534 this._rfbCredentials.target === undefined) {
1535 this.dispatchEvent(new CustomEvent(
1536 "credentialsrequired",
1537 { detail: { types: ["username", "password", "target"] } }));
1541 this._sock.sQpush8(this._rfbCredentials.username.length);
1542 this._sock.sQpush8(this._rfbCredentials.target.length);
1543 this._sock.sQpushString(this._rfbCredentials.username);
1544 this._sock.sQpushString(this._rfbCredentials.target);
1548 this._rfbAuthScheme = securityTypeVNCAuth;
1550 return this._negotiateAuthentication();
1553 // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
1554 _negotiateVeNCryptAuth() {
1556 // waiting for VeNCrypt version
1557 if (this._rfbVeNCryptState == 0) {
1558 if (this._sock.rQwait("vencrypt version", 2)) { return false; }
1560 const major = this._sock.rQshift8();
1561 const minor = this._sock.rQshift8();
1563 if (!(major == 0 && minor == 2)) {
1564 return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
1567 this._sock.sQpush8(0);
1568 this._sock.sQpush8(2);
1570 this._rfbVeNCryptState = 1;
1574 if (this._rfbVeNCryptState == 1) {
1575 if (this._sock.rQwait("vencrypt ack", 1)) { return false; }
1577 const res = this._sock.rQshift8();
1580 return this._fail("VeNCrypt failure " + res);
1583 this._rfbVeNCryptState = 2;
1585 // must fall through here (i.e. no "else if"), beacause we may have already received
1586 // the subtypes length and won't be called again
1588 if (this._rfbVeNCryptState == 2) { // waiting for subtypes length
1589 if (this._sock.rQwait("vencrypt subtypes length", 1)) { return false; }
1591 const subtypesLength = this._sock.rQshift8();
1592 if (subtypesLength < 1) {
1593 return this._fail("VeNCrypt subtypes empty");
1596 this._rfbVeNCryptSubtypesLength = subtypesLength;
1597 this._rfbVeNCryptState = 3;
1600 // waiting for subtypes list
1601 if (this._rfbVeNCryptState == 3) {
1602 if (this._sock.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength)) { return false; }
1604 const subtypes = [];
1605 for (let i = 0; i < this._rfbVeNCryptSubtypesLength; i++) {
1606 subtypes.push(this._sock.rQshift32());
1609 // Look for a matching security type in the order that the
1611 this._rfbAuthScheme = -1;
1612 for (let type of subtypes) {
1613 // Avoid getting in to a loop
1614 if (type === securityTypeVeNCrypt) {
1618 if (this._isSupportedSecurityType(type)) {
1619 this._rfbAuthScheme = type;
1624 if (this._rfbAuthScheme === -1) {
1625 return this._fail("Unsupported security types (types: " + subtypes + ")");
1628 this._sock.sQpush32(this._rfbAuthScheme);
1631 this._rfbVeNCryptState = 4;
1636 _negotiatePlainAuth() {
1637 if (this._rfbCredentials.username === undefined ||
1638 this._rfbCredentials.password === undefined) {
1639 this.dispatchEvent(new CustomEvent(
1640 "credentialsrequired",
1641 { detail: { types: ["username", "password"] } }));
1645 const user = encodeUTF8(this._rfbCredentials.username);
1646 const pass = encodeUTF8(this._rfbCredentials.password);
1648 this._sock.sQpush32(user.length);
1649 this._sock.sQpush32(pass.length);
1650 this._sock.sQpushString(user);
1651 this._sock.sQpushString(pass);
1654 this._rfbInitState = "SecurityResult";
1658 _negotiateStdVNCAuth() {
1659 if (this._sock.rQwait("auth challenge", 16)) { return false; }
1661 if (this._rfbCredentials.password === undefined) {
1662 this.dispatchEvent(new CustomEvent(
1663 "credentialsrequired",
1664 { detail: { types: ["password"] } }));
1668 // TODO(directxman12): make genDES not require an Array
1669 const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
1670 const response = RFB.genDES(this._rfbCredentials.password, challenge);
1671 this._sock.sQpushBytes(response);
1673 this._rfbInitState = "SecurityResult";
1677 _negotiateARDAuth() {
1679 if (this._rfbCredentials.username === undefined ||
1680 this._rfbCredentials.password === undefined) {
1681 this.dispatchEvent(new CustomEvent(
1682 "credentialsrequired",
1683 { detail: { types: ["username", "password"] } }));
1687 if (this._rfbCredentials.ardPublicKey != undefined &&
1688 this._rfbCredentials.ardCredentials != undefined) {
1689 // if the async web crypto is done return the results
1690 this._sock.sQpushBytes(this._rfbCredentials.ardCredentials);
1691 this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey);
1693 this._rfbCredentials.ardCredentials = null;
1694 this._rfbCredentials.ardPublicKey = null;
1695 this._rfbInitState = "SecurityResult";
1699 if (this._sock.rQwait("read ard", 4)) { return false; }
1701 let generator = this._sock.rQshiftBytes(2); // DH base generator value
1703 let keyLength = this._sock.rQshift16();
1705 if (this._sock.rQwait("read ard keylength", keyLength*2, 4)) { return false; }
1707 // read the server values
1708 let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus
1709 let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
1711 let clientKey = legacyCrypto.generateKey(
1712 { name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
1713 this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);
1718 async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {
1719 const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
1720 const sharedKey = legacyCrypto.deriveBits(
1721 { name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);
1723 const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
1724 const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
1726 const credentials = window.crypto.getRandomValues(new Uint8Array(128));
1727 for (let i = 0; i < username.length; i++) {
1728 credentials[i] = username.charCodeAt(i);
1730 credentials[username.length] = 0;
1731 for (let i = 0; i < password.length; i++) {
1732 credentials[64 + i] = password.charCodeAt(i);
1734 credentials[64 + password.length] = 0;
1736 const key = await legacyCrypto.digest("MD5", sharedKey);
1737 const cipher = await legacyCrypto.importKey(
1738 "raw", key, { name: "AES-ECB" }, false, ["encrypt"]);
1739 const encrypted = await legacyCrypto.encrypt({ name: "AES-ECB" }, cipher, credentials);
1741 this._rfbCredentials.ardCredentials = encrypted;
1742 this._rfbCredentials.ardPublicKey = clientPublicKey;
1744 this._resumeAuthentication();
1747 _negotiateTightUnixAuth() {
1748 if (this._rfbCredentials.username === undefined ||
1749 this._rfbCredentials.password === undefined) {
1750 this.dispatchEvent(new CustomEvent(
1751 "credentialsrequired",
1752 { detail: { types: ["username", "password"] } }));
1756 this._sock.sQpush32(this._rfbCredentials.username.length);
1757 this._sock.sQpush32(this._rfbCredentials.password.length);
1758 this._sock.sQpushString(this._rfbCredentials.username);
1759 this._sock.sQpushString(this._rfbCredentials.password);
1762 this._rfbInitState = "SecurityResult";
1766 _negotiateTightTunnels(numTunnels) {
1767 const clientSupportedTunnelTypes = {
1768 0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
1770 const serverSupportedTunnelTypes = {};
1771 // receive tunnel capabilities
1772 for (let i = 0; i < numTunnels; i++) {
1773 const capCode = this._sock.rQshift32();
1774 const capVendor = this._sock.rQshiftStr(4);
1775 const capSignature = this._sock.rQshiftStr(8);
1776 serverSupportedTunnelTypes[capCode] = { vendor: capVendor, signature: capSignature };
1779 Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes);
1781 // Siemens touch panels have a VNC server that supports NOTUNNEL,
1782 // but forgets to advertise it. Try to detect such servers by
1783 // looking for their custom tunnel type.
1784 if (serverSupportedTunnelTypes[1] &&
1785 (serverSupportedTunnelTypes[1].vendor === "SICR") &&
1786 (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) {
1787 Log.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
1788 serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };
1791 // choose the notunnel type
1792 if (serverSupportedTunnelTypes[0]) {
1793 if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
1794 serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
1795 return this._fail("Client's tunnel type had the incorrect " +
1796 "vendor or signature");
1798 Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
1799 this._sock.sQpush32(0); // use NOTUNNEL
1801 return false; // wait until we receive the sub auth count to continue
1803 return this._fail("Server wanted tunnels, but doesn't support " +
1804 "the notunnel type");
1808 _negotiateTightAuth() {
1809 if (!this._rfbTightVNC) { // first pass, do the tunnel negotiation
1810 if (this._sock.rQwait("num tunnels", 4)) { return false; }
1811 const numTunnels = this._sock.rQshift32();
1812 if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
1814 this._rfbTightVNC = true;
1816 if (numTunnels > 0) {
1817 this._negotiateTightTunnels(numTunnels);
1818 return false; // wait until we receive the sub auth to continue
1822 // second pass, do the sub-auth negotiation
1823 if (this._sock.rQwait("sub auth count", 4)) { return false; }
1824 const subAuthCount = this._sock.rQshift32();
1825 if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected
1826 this._rfbInitState = 'SecurityResult';
1830 if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
1832 const clientSupportedTypes = {
1838 const serverSupportedTypes = [];
1840 for (let i = 0; i < subAuthCount; i++) {
1841 this._sock.rQshift32(); // capNum
1842 const capabilities = this._sock.rQshiftStr(12);
1843 serverSupportedTypes.push(capabilities);
1846 Log.Debug("Server Tight authentication types: " + serverSupportedTypes);
1848 for (let authType in clientSupportedTypes) {
1849 if (serverSupportedTypes.indexOf(authType) != -1) {
1850 this._sock.sQpush32(clientSupportedTypes[authType]);
1852 Log.Debug("Selected authentication type: " + authType);
1855 case 'STDVNOAUTH__': // no auth
1856 this._rfbInitState = 'SecurityResult';
1858 case 'STDVVNCAUTH_':
1859 this._rfbAuthScheme = securityTypeVNCAuth;
1861 case 'TGHTULGNAUTH':
1862 this._rfbAuthScheme = securityTypeUnixLogon;
1865 return this._fail("Unsupported tiny auth scheme " +
1866 "(scheme: " + authType + ")");
1871 return this._fail("No supported sub-auth types!");
1874 _handleRSAAESCredentialsRequired(event) {
1875 this.dispatchEvent(event);
1878 _handleRSAAESServerVerification(event) {
1879 this.dispatchEvent(event);
1882 _negotiateRA2neAuth() {
1883 if (this._rfbRSAAESAuthenticationState === null) {
1884 this._rfbRSAAESAuthenticationState = new RSAAESAuthenticationState(this._sock, () => this._rfbCredentials);
1885 this._rfbRSAAESAuthenticationState.addEventListener(
1886 "serververification", this._eventHandlers.handleRSAAESServerVerification);
1887 this._rfbRSAAESAuthenticationState.addEventListener(
1888 "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
1890 this._rfbRSAAESAuthenticationState.checkInternalEvents();
1891 if (!this._rfbRSAAESAuthenticationState.hasStarted) {
1892 this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync()
1894 if (e.message !== "disconnect normally") {
1895 this._fail(e.message);
1899 this._rfbInitState = "SecurityResult";
1902 this._rfbRSAAESAuthenticationState.removeEventListener(
1903 "serververification", this._eventHandlers.handleRSAAESServerVerification);
1904 this._rfbRSAAESAuthenticationState.removeEventListener(
1905 "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
1906 this._rfbRSAAESAuthenticationState = null;
1912 _negotiateMSLogonIIAuth() {
1913 if (this._sock.rQwait("mslogonii dh param", 24)) { return false; }
1915 if (this._rfbCredentials.username === undefined ||
1916 this._rfbCredentials.password === undefined) {
1917 this.dispatchEvent(new CustomEvent(
1918 "credentialsrequired",
1919 { detail: { types: ["username", "password"] } }));
1923 const g = this._sock.rQshiftBytes(8);
1924 const p = this._sock.rQshiftBytes(8);
1925 const A = this._sock.rQshiftBytes(8);
1926 const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]);
1927 const B = legacyCrypto.exportKey("raw", dhKey.publicKey);
1928 const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64);
1930 const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]);
1931 const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
1932 const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
1933 let usernameBytes = new Uint8Array(256);
1934 let passwordBytes = new Uint8Array(64);
1935 window.crypto.getRandomValues(usernameBytes);
1936 window.crypto.getRandomValues(passwordBytes);
1937 for (let i = 0; i < username.length; i++) {
1938 usernameBytes[i] = username.charCodeAt(i);
1940 usernameBytes[username.length] = 0;
1941 for (let i = 0; i < password.length; i++) {
1942 passwordBytes[i] = password.charCodeAt(i);
1944 passwordBytes[password.length] = 0;
1945 usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes);
1946 passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes);
1947 this._sock.sQpushBytes(B);
1948 this._sock.sQpushBytes(usernameBytes);
1949 this._sock.sQpushBytes(passwordBytes);
1951 this._rfbInitState = "SecurityResult";
1955 _negotiateAuthentication() {
1956 switch (this._rfbAuthScheme) {
1957 case securityTypeNone:
1958 if (this._rfbVersion >= 3.8) {
1959 this._rfbInitState = 'SecurityResult';
1961 this._rfbInitState = 'ClientInitialisation';
1965 case securityTypeXVP:
1966 return this._negotiateXvpAuth();
1968 case securityTypeARD:
1969 return this._negotiateARDAuth();
1971 case securityTypeVNCAuth:
1972 return this._negotiateStdVNCAuth();
1974 case securityTypeTight:
1975 return this._negotiateTightAuth();
1977 case securityTypeVeNCrypt:
1978 return this._negotiateVeNCryptAuth();
1980 case securityTypePlain:
1981 return this._negotiatePlainAuth();
1983 case securityTypeUnixLogon:
1984 return this._negotiateTightUnixAuth();
1986 case securityTypeRA2ne:
1987 return this._negotiateRA2neAuth();
1989 case securityTypeMSLogonII:
1990 return this._negotiateMSLogonIIAuth();
1993 return this._fail("Unsupported auth scheme (scheme: " +
1994 this._rfbAuthScheme + ")");
1998 _handleSecurityResult() {
1999 if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
2001 const status = this._sock.rQshift32();
2003 if (status === 0) { // OK
2004 this._rfbInitState = 'ClientInitialisation';
2005 Log.Debug('Authentication OK');
2008 if (this._rfbVersion >= 3.8) {
2009 this._rfbInitState = "SecurityReason";
2010 this._securityContext = "security result";
2011 this._securityStatus = status;
2014 this.dispatchEvent(new CustomEvent(
2016 { detail: { status: status } }));
2018 return this._fail("Security handshake failed");
2023 _negotiateServerInit() {
2024 if (this._sock.rQwait("server initialization", 24)) { return false; }
2027 const width = this._sock.rQshift16();
2028 const height = this._sock.rQshift16();
2031 const bpp = this._sock.rQshift8();
2032 const depth = this._sock.rQshift8();
2033 const bigEndian = this._sock.rQshift8();
2034 const trueColor = this._sock.rQshift8();
2036 const redMax = this._sock.rQshift16();
2037 const greenMax = this._sock.rQshift16();
2038 const blueMax = this._sock.rQshift16();
2039 const redShift = this._sock.rQshift8();
2040 const greenShift = this._sock.rQshift8();
2041 const blueShift = this._sock.rQshift8();
2042 this._sock.rQskipBytes(3); // padding
2044 // NB(directxman12): we don't want to call any callbacks or print messages until
2045 // *after* we're past the point where we could backtrack
2047 /* Connection name/title */
2048 const nameLength = this._sock.rQshift32();
2049 if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
2050 let name = this._sock.rQshiftStr(nameLength);
2051 name = decodeUTF8(name, true);
2053 if (this._rfbTightVNC) {
2054 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
2055 // In TightVNC mode, ServerInit message is extended
2056 const numServerMessages = this._sock.rQshift16();
2057 const numClientMessages = this._sock.rQshift16();
2058 const numEncodings = this._sock.rQshift16();
2059 this._sock.rQskipBytes(2); // padding
2061 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
2062 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
2064 // we don't actually do anything with the capability information that TIGHT sends,
2065 // so we just skip the all of this.
2067 // TIGHT server message capabilities
2068 this._sock.rQskipBytes(16 * numServerMessages);
2070 // TIGHT client message capabilities
2071 this._sock.rQskipBytes(16 * numClientMessages);
2073 // TIGHT encoding capabilities
2074 this._sock.rQskipBytes(16 * numEncodings);
2077 // NB(directxman12): these are down here so that we don't run them multiple times
2079 Log.Info("Screen: " + width + "x" + height +
2080 ", bpp: " + bpp + ", depth: " + depth +
2081 ", bigEndian: " + bigEndian +
2082 ", trueColor: " + trueColor +
2083 ", redMax: " + redMax +
2084 ", greenMax: " + greenMax +
2085 ", blueMax: " + blueMax +
2086 ", redShift: " + redShift +
2087 ", greenShift: " + greenShift +
2088 ", blueShift: " + blueShift);
2090 // we're past the point where we could backtrack, so it's safe to call this
2091 this._setDesktopName(name);
2092 this._resize(width, height);
2094 if (!this._viewOnly) { this._keyboard.grab(); }
2098 if (this._fbName === "Intel(r) AMT KVM") {
2099 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
2103 RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
2104 this._sendEncodings();
2105 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
2107 this._updateConnectionState('connected');
2114 // In preference order
2115 encs.push(encodings.encodingCopyRect);
2116 // Only supported with full depth support
2117 if (this._fbDepth == 24) {
2118 encs.push(encodings.encodingTight);
2119 encs.push(encodings.encodingTightPNG);
2120 encs.push(encodings.encodingZRLE);
2121 encs.push(encodings.encodingJPEG);
2122 encs.push(encodings.encodingHextile);
2123 encs.push(encodings.encodingRRE);
2125 encs.push(encodings.encodingRaw);
2127 // Psuedo-encoding settings
2128 encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
2129 encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
2131 encs.push(encodings.pseudoEncodingDesktopSize);
2132 encs.push(encodings.pseudoEncodingLastRect);
2133 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
2134 encs.push(encodings.pseudoEncodingQEMULedEvent);
2135 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
2136 encs.push(encodings.pseudoEncodingXvp);
2137 encs.push(encodings.pseudoEncodingFence);
2138 encs.push(encodings.pseudoEncodingContinuousUpdates);
2139 encs.push(encodings.pseudoEncodingDesktopName);
2140 encs.push(encodings.pseudoEncodingExtendedClipboard);
2142 if (this._fbDepth == 24) {
2143 encs.push(encodings.pseudoEncodingVMwareCursor);
2144 encs.push(encodings.pseudoEncodingCursor);
2147 RFB.messages.clientEncodings(this._sock, encs);
2150 /* RFB protocol initialization states:
2155 * ClientInitialization - not triggered by server message
2156 * ServerInitialization
2159 switch (this._rfbInitState) {
2160 case 'ProtocolVersion':
2161 return this._negotiateProtocolVersion();
2164 return this._negotiateSecurity();
2166 case 'Authentication':
2167 return this._negotiateAuthentication();
2169 case 'SecurityResult':
2170 return this._handleSecurityResult();
2172 case 'SecurityReason':
2173 return this._handleSecurityReason();
2175 case 'ClientInitialisation':
2176 this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation
2178 this._rfbInitState = 'ServerInitialisation';
2181 case 'ServerInitialisation':
2182 return this._negotiateServerInit();
2185 return this._fail("Unknown init state (state: " +
2186 this._rfbInitState + ")");
2190 // Resume authentication handshake after it was paused for some
2191 // reason, e.g. waiting for a password from the user
2192 _resumeAuthentication() {
2193 // We use setTimeout() so it's run in its own context, just like
2194 // it originally did via the WebSocket's event handler
2195 setTimeout(this._initMsg.bind(this), 0);
2198 _handleSetColourMapMsg() {
2199 Log.Debug("SetColorMapEntries");
2201 return this._fail("Unexpected SetColorMapEntries message");
2204 _handleServerCutText() {
2205 Log.Debug("ServerCutText");
2207 if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
2209 this._sock.rQskipBytes(3); // Padding
2211 let length = this._sock.rQshift32();
2212 length = toSigned32bit(length);
2214 if (this._sock.rQwait("ServerCutText content", Math.abs(length), 8)) { return false; }
2218 const text = this._sock.rQshiftStr(length);
2219 if (this._viewOnly) {
2223 this.dispatchEvent(new CustomEvent(
2225 { detail: { text: text } }));
2229 length = Math.abs(length);
2230 const flags = this._sock.rQshift32();
2231 let formats = flags & 0x0000FFFF;
2232 let actions = flags & 0xFF000000;
2234 let isCaps = (!!(actions & extendedClipboardActionCaps));
2236 this._clipboardServerCapabilitiesFormats = {};
2237 this._clipboardServerCapabilitiesActions = {};
2239 // Update our server capabilities for Formats
2240 for (let i = 0; i <= 15; i++) {
2243 // Check if format flag is set.
2244 if ((formats & index)) {
2245 this._clipboardServerCapabilitiesFormats[index] = true;
2246 // We don't send unsolicited clipboard, so we
2248 this._sock.rQshift32();
2252 // Update our server capabilities for Actions
2253 for (let i = 24; i <= 31; i++) {
2255 this._clipboardServerCapabilitiesActions[index] = !!(actions & index);
2258 /* Caps handling done, send caps with the clients
2259 capabilities set as a response */
2260 let clientActions = [
2261 extendedClipboardActionCaps,
2262 extendedClipboardActionRequest,
2263 extendedClipboardActionPeek,
2264 extendedClipboardActionNotify,
2265 extendedClipboardActionProvide
2267 RFB.messages.extendedClipboardCaps(this._sock, clientActions, {extendedClipboardFormatText: 0});
2269 } else if (actions === extendedClipboardActionRequest) {
2270 if (this._viewOnly) {
2274 // Check if server has told us it can handle Provide and there is clipboard data to send.
2275 if (this._clipboardText != null &&
2276 this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) {
2278 if (formats & extendedClipboardFormatText) {
2279 RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]);
2283 } else if (actions === extendedClipboardActionPeek) {
2284 if (this._viewOnly) {
2288 if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
2290 if (this._clipboardText != null) {
2291 RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
2293 RFB.messages.extendedClipboardNotify(this._sock, []);
2297 } else if (actions === extendedClipboardActionNotify) {
2298 if (this._viewOnly) {
2302 if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) {
2304 if (formats & extendedClipboardFormatText) {
2305 RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]);
2309 } else if (actions === extendedClipboardActionProvide) {
2310 if (this._viewOnly) {
2314 if (!(formats & extendedClipboardFormatText)) {
2317 // Ignore what we had in our clipboard client side.
2318 this._clipboardText = null;
2320 // FIXME: Should probably verify that this data was actually requested
2321 let zlibStream = this._sock.rQshiftBytes(length - 4);
2322 let streamInflator = new Inflator();
2323 let textData = null;
2325 streamInflator.setInput(zlibStream);
2326 for (let i = 0; i <= 15; i++) {
2327 let format = 1 << i;
2329 if (formats & format) {
2332 let sizeArray = streamInflator.inflate(4);
2334 size |= (sizeArray[0] << 24);
2335 size |= (sizeArray[1] << 16);
2336 size |= (sizeArray[2] << 8);
2337 size |= (sizeArray[3]);
2338 let chunk = streamInflator.inflate(size);
2340 if (format === extendedClipboardFormatText) {
2345 streamInflator.setInput(null);
2347 if (textData !== null) {
2349 for (let i = 0; i < textData.length; i++) {
2350 tmpText += String.fromCharCode(textData[i]);
2354 textData = decodeUTF8(textData);
2355 if ((textData.length > 0) && "\0" === textData.charAt(textData.length - 1)) {
2356 textData = textData.slice(0, -1);
2359 textData = textData.replaceAll("\r\n", "\n");
2361 this.dispatchEvent(new CustomEvent(
2363 { detail: { text: textData } }));
2366 return this._fail("Unexpected action in extended clipboard message: " + actions);
2372 _handleServerFenceMsg() {
2373 if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; }
2374 this._sock.rQskipBytes(3); // Padding
2375 let flags = this._sock.rQshift32();
2376 let length = this._sock.rQshift8();
2378 if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; }
2381 Log.Warn("Bad payload length (" + length + ") in fence response");
2385 const payload = this._sock.rQshiftStr(length);
2387 this._supportsFence = true;
2392 * (1<<0) - BlockBefore
2393 * (1<<1) - BlockAfter
2398 if (!(flags & (1<<31))) {
2399 return this._fail("Unexpected fence response");
2402 // Filter out unsupported flags
2403 // FIXME: support syncNext
2404 flags &= (1<<0) | (1<<1);
2406 // BlockBefore and BlockAfter are automatically handled by
2407 // the fact that we process each incoming message
2409 RFB.messages.clientFence(this._sock, flags, payload);
2415 if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
2416 this._sock.rQskipBytes(1); // Padding
2417 const xvpVer = this._sock.rQshift8();
2418 const xvpMsg = this._sock.rQshift8();
2422 Log.Error("XVP Operation Failed");
2425 this._rfbXvpVer = xvpVer;
2426 Log.Info("XVP extensions enabled (version " + this._rfbXvpVer + ")");
2427 this._setCapability("power", true);
2430 this._fail("Illegal server XVP message (msg: " + xvpMsg + ")");
2439 if (this._FBU.rects > 0) {
2442 msgType = this._sock.rQshift8();
2447 case 0: // FramebufferUpdate
2448 ret = this._framebufferUpdate();
2449 if (ret && !this._enabledContinuousUpdates) {
2450 RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,
2451 this._fbWidth, this._fbHeight);
2455 case 1: // SetColorMapEntries
2456 return this._handleSetColourMapMsg();
2460 this.dispatchEvent(new CustomEvent(
2465 case 3: // ServerCutText
2466 return this._handleServerCutText();
2468 case 150: // EndOfContinuousUpdates
2469 first = !this._supportsContinuousUpdates;
2470 this._supportsContinuousUpdates = true;
2471 this._enabledContinuousUpdates = false;
2473 this._enabledContinuousUpdates = true;
2474 this._updateContinuousUpdates();
2475 Log.Info("Enabling continuous updates.");
2477 // FIXME: We need to send a framebufferupdaterequest here
2478 // if we add support for turning off continuous updates
2482 case 248: // ServerFence
2483 return this._handleServerFenceMsg();
2486 return this._handleXvpMsg();
2489 this._fail("Unexpected server message (type " + msgType + ")");
2490 Log.Debug("sock.rQpeekBytes(30): " + this._sock.rQpeekBytes(30));
2495 _framebufferUpdate() {
2496 if (this._FBU.rects === 0) {
2497 if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
2498 this._sock.rQskipBytes(1); // Padding
2499 this._FBU.rects = this._sock.rQshift16();
2501 // Make sure the previous frame is fully rendered first
2502 // to avoid building up an excessive queue
2503 if (this._display.pending()) {
2504 this._flushing = true;
2505 this._display.flush()
2507 this._flushing = false;
2508 // Resume processing
2509 if (!this._sock.rQwait("message", 1)) {
2510 this._handleMessage();
2517 while (this._FBU.rects > 0) {
2518 if (this._FBU.encoding === null) {
2519 if (this._sock.rQwait("rect header", 12)) { return false; }
2520 /* New FramebufferUpdate */
2522 this._FBU.x = this._sock.rQshift16();
2523 this._FBU.y = this._sock.rQshift16();
2524 this._FBU.width = this._sock.rQshift16();
2525 this._FBU.height = this._sock.rQshift16();
2526 this._FBU.encoding = this._sock.rQshift32();
2527 /* Encodings are signed */
2528 this._FBU.encoding >>= 0;
2531 if (!this._handleRect()) {
2536 this._FBU.encoding = null;
2539 this._display.flip();
2541 return true; // We finished this FBU
2545 switch (this._FBU.encoding) {
2546 case encodings.pseudoEncodingLastRect:
2547 this._FBU.rects = 1; // Will be decreased when we return
2550 case encodings.pseudoEncodingVMwareCursor:
2551 return this._handleVMwareCursor();
2553 case encodings.pseudoEncodingCursor:
2554 return this._handleCursor();
2556 case encodings.pseudoEncodingQEMUExtendedKeyEvent:
2557 this._qemuExtKeyEventSupported = true;
2560 case encodings.pseudoEncodingDesktopName:
2561 return this._handleDesktopName();
2563 case encodings.pseudoEncodingDesktopSize:
2564 this._resize(this._FBU.width, this._FBU.height);
2567 case encodings.pseudoEncodingExtendedDesktopSize:
2568 return this._handleExtendedDesktopSize();
2570 case encodings.pseudoEncodingQEMULedEvent:
2571 return this._handleLedEvent();
2574 return this._handleDataRect();
2578 _handleVMwareCursor() {
2579 const hotx = this._FBU.x; // hotspot-x
2580 const hoty = this._FBU.y; // hotspot-y
2581 const w = this._FBU.width;
2582 const h = this._FBU.height;
2583 if (this._sock.rQwait("VMware cursor encoding", 1)) {
2587 const cursorType = this._sock.rQshift8();
2589 this._sock.rQshift8(); //Padding
2592 const bytesPerPixel = 4;
2595 if (cursorType == 0) {
2596 //Used to filter away unimportant bits.
2597 //OR is used for correct conversion in js.
2598 const PIXEL_MASK = 0xffffff00 | 0;
2599 rgba = new Array(w * h * bytesPerPixel);
2601 if (this._sock.rQwait("VMware cursor classic encoding",
2602 (w * h * bytesPerPixel) * 2, 2)) {
2606 let andMask = new Array(w * h);
2607 for (let pixel = 0; pixel < (w * h); pixel++) {
2608 andMask[pixel] = this._sock.rQshift32();
2611 let xorMask = new Array(w * h);
2612 for (let pixel = 0; pixel < (w * h); pixel++) {
2613 xorMask[pixel] = this._sock.rQshift32();
2616 for (let pixel = 0; pixel < (w * h); pixel++) {
2617 if (andMask[pixel] == 0) {
2618 //Fully opaque pixel
2619 let bgr = xorMask[pixel];
2620 let r = bgr >> 8 & 0xff;
2621 let g = bgr >> 16 & 0xff;
2622 let b = bgr >> 24 & 0xff;
2624 rgba[(pixel * bytesPerPixel) ] = r; //r
2625 rgba[(pixel * bytesPerPixel) + 1 ] = g; //g
2626 rgba[(pixel * bytesPerPixel) + 2 ] = b; //b
2627 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff; //a
2629 } else if ((andMask[pixel] & PIXEL_MASK) ==
2631 //Only screen value matters, no mouse colouring
2632 if (xorMask[pixel] == 0) {
2634 rgba[(pixel * bytesPerPixel) ] = 0x00;
2635 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2636 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2637 rgba[(pixel * bytesPerPixel) + 3 ] = 0x00;
2639 } else if ((xorMask[pixel] & PIXEL_MASK) ==
2641 //Inverted pixel, not supported in browsers.
2642 //Fully opaque instead.
2643 rgba[(pixel * bytesPerPixel) ] = 0x00;
2644 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2645 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2646 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
2650 rgba[(pixel * bytesPerPixel) ] = 0x00;
2651 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2652 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2653 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
2658 rgba[(pixel * bytesPerPixel) ] = 0x00;
2659 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2660 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2661 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
2666 } else if (cursorType == 1) {
2667 if (this._sock.rQwait("VMware cursor alpha encoding",
2672 rgba = new Array(w * h * bytesPerPixel);
2674 for (let pixel = 0; pixel < (w * h); pixel++) {
2675 let data = this._sock.rQshift32();
2677 rgba[(pixel * 4) ] = data >> 24 & 0xff; //r
2678 rgba[(pixel * 4) + 1 ] = data >> 16 & 0xff; //g
2679 rgba[(pixel * 4) + 2 ] = data >> 8 & 0xff; //b
2680 rgba[(pixel * 4) + 3 ] = data & 0xff; //a
2684 Log.Warn("The given cursor type is not supported: "
2685 + cursorType + " given.");
2689 this._updateCursor(rgba, hotx, hoty, w, h);
2695 const hotx = this._FBU.x; // hotspot-x
2696 const hoty = this._FBU.y; // hotspot-y
2697 const w = this._FBU.width;
2698 const h = this._FBU.height;
2700 const pixelslength = w * h * 4;
2701 const masklength = Math.ceil(w / 8) * h;
2703 let bytes = pixelslength + masklength;
2704 if (this._sock.rQwait("cursor encoding", bytes)) {
2708 // Decode from BGRX pixels + bit mask to RGBA
2709 const pixels = this._sock.rQshiftBytes(pixelslength);
2710 const mask = this._sock.rQshiftBytes(masklength);
2711 let rgba = new Uint8Array(w * h * 4);
2714 for (let y = 0; y < h; y++) {
2715 for (let x = 0; x < w; x++) {
2716 let maskIdx = y * Math.ceil(w / 8) + Math.floor(x / 8);
2717 let alpha = (mask[maskIdx] << (x % 8)) & 0x80 ? 255 : 0;
2718 rgba[pixIdx ] = pixels[pixIdx + 2];
2719 rgba[pixIdx + 1] = pixels[pixIdx + 1];
2720 rgba[pixIdx + 2] = pixels[pixIdx];
2721 rgba[pixIdx + 3] = alpha;
2726 this._updateCursor(rgba, hotx, hoty, w, h);
2731 _handleDesktopName() {
2732 if (this._sock.rQwait("DesktopName", 4)) {
2736 let length = this._sock.rQshift32();
2738 if (this._sock.rQwait("DesktopName", length, 4)) {
2742 let name = this._sock.rQshiftStr(length);
2743 name = decodeUTF8(name, true);
2745 this._setDesktopName(name);
2751 if (this._sock.rQwait("LED Status", 1)) {
2755 let data = this._sock.rQshift8();
2756 // ScrollLock state can be retrieved with data & 1. This is currently not needed.
2757 let numLock = data & 2 ? true : false;
2758 let capsLock = data & 4 ? true : false;
2759 this._remoteCapsLock = capsLock;
2760 this._remoteNumLock = numLock;
2765 _handleExtendedDesktopSize() {
2766 if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
2770 const numberOfScreens = this._sock.rQpeek8();
2772 let bytes = 4 + (numberOfScreens * 16);
2773 if (this._sock.rQwait("ExtendedDesktopSize", bytes)) {
2777 const firstUpdate = !this._supportsSetDesktopSize;
2778 this._supportsSetDesktopSize = true;
2780 this._sock.rQskipBytes(1); // number-of-screens
2781 this._sock.rQskipBytes(3); // padding
2783 for (let i = 0; i < numberOfScreens; i += 1) {
2784 // Save the id and flags of the first screen
2786 this._screenID = this._sock.rQshift32(); // id
2787 this._sock.rQskipBytes(2); // x-position
2788 this._sock.rQskipBytes(2); // y-position
2789 this._sock.rQskipBytes(2); // width
2790 this._sock.rQskipBytes(2); // height
2791 this._screenFlags = this._sock.rQshift32(); // flags
2793 this._sock.rQskipBytes(16);
2798 * The x-position indicates the reason for the change:
2800 * 0 - server resized on its own
2801 * 1 - this client requested the resize
2802 * 2 - another client requested the resize
2805 // We need to handle errors when we requested the resize.
2806 if (this._FBU.x === 1 && this._FBU.y !== 0) {
2808 // The y-position indicates the status code from the server
2809 switch (this._FBU.y) {
2811 msg = "Resize is administratively prohibited";
2814 msg = "Out of resources";
2817 msg = "Invalid screen layout";
2820 msg = "Unknown reason";
2823 Log.Warn("Server did not accept the resize request: "
2826 this._resize(this._FBU.width, this._FBU.height);
2829 // Normally we only apply the current resize mode after a
2830 // window resize event. However there is no such trigger on the
2831 // initial connect. And we don't know if the server supports
2832 // resizing until we've gotten here.
2834 this._requestRemoteResize();
2841 let decoder = this._decoders[this._FBU.encoding];
2843 this._fail("Unsupported encoding (encoding: " +
2844 this._FBU.encoding + ")");
2849 return decoder.decodeRect(this._FBU.x, this._FBU.y,
2850 this._FBU.width, this._FBU.height,
2851 this._sock, this._display,
2854 this._fail("Error decoding rect: " + err);
2859 _updateContinuousUpdates() {
2860 if (!this._enabledContinuousUpdates) { return; }
2862 RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
2863 this._fbWidth, this._fbHeight);
2866 _resize(width, height) {
2867 this._fbWidth = width;
2868 this._fbHeight = height;
2870 this._display.resize(this._fbWidth, this._fbHeight);
2872 // Adjust the visible viewport based on the new dimensions
2874 this._updateScale();
2876 this._updateContinuousUpdates();
2878 // Keep this size until browser client size changes
2879 this._saveExpectedClientSize();
2883 if (this._rfbXvpVer < ver) { return; }
2884 Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
2885 RFB.messages.xvpOp(this._sock, ver, op);
2888 _updateCursor(rgba, hotx, hoty, w, h) {
2889 this._cursorImage = {
2891 hotx: hotx, hoty: hoty, w: w, h: h,
2893 this._refreshCursor();
2896 _shouldShowDotCursor() {
2897 // Called when this._cursorImage is updated
2898 if (!this._showDotCursor) {
2899 // User does not want to see the dot, so...
2903 // The dot should not be shown if the cursor is already visible,
2904 // i.e. contains at least one not-fully-transparent pixel.
2905 // So iterate through all alpha bytes in rgba and stop at the
2907 for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) {
2908 if (this._cursorImage.rgbaPixels[i]) {
2913 // At this point, we know that the cursor is fully transparent, and
2914 // the user wants to see the dot instead of this.
2919 if (this._rfbConnectionState !== "connecting" &&
2920 this._rfbConnectionState !== "connected") {
2923 const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage;
2924 this._cursor.change(image.rgbaPixels,
2925 image.hotx, image.hoty,
2930 static genDES(password, challenge) {
2931 const passwordChars = password.split('').map(c => c.charCodeAt(0));
2932 const key = legacyCrypto.importKey(
2933 "raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]);
2934 return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge);
2940 keyEvent(sock, keysym, down) {
2941 sock.sQpush8(4); // msg-type
2946 sock.sQpush32(keysym);
2951 QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
2952 function getRFBkeycode(xtScanCode) {
2953 const upperByte = (keycode >> 8);
2954 const lowerByte = (keycode & 0x00ff);
2955 if (upperByte === 0xe0 && lowerByte < 0x7f) {
2956 return lowerByte | 0x80;
2961 sock.sQpush8(255); // msg-type
2962 sock.sQpush8(0); // sub msg-type
2964 sock.sQpush16(down);
2966 sock.sQpush32(keysym);
2968 const RFBkeycode = getRFBkeycode(keycode);
2970 sock.sQpush32(RFBkeycode);
2975 pointerEvent(sock, x, y, mask) {
2976 sock.sQpush8(5); // msg-type
2986 // Used to build Notify and Request data.
2987 _buildExtendedClipboardFlags(actions, formats) {
2988 let data = new Uint8Array(4);
2989 let formatFlag = 0x00000000;
2990 let actionFlag = 0x00000000;
2992 for (let i = 0; i < actions.length; i++) {
2993 actionFlag |= actions[i];
2996 for (let i = 0; i < formats.length; i++) {
2997 formatFlag |= formats[i];
3000 data[0] = actionFlag >> 24; // Actions
3001 data[1] = 0x00; // Reserved
3002 data[2] = 0x00; // Reserved
3003 data[3] = formatFlag; // Formats
3008 extendedClipboardProvide(sock, formats, inData) {
3009 // Deflate incomming data and their sizes
3010 let deflator = new Deflator();
3011 let dataToDeflate = [];
3013 for (let i = 0; i < formats.length; i++) {
3014 // We only support the format Text at this time
3015 if (formats[i] != extendedClipboardFormatText) {
3016 throw new Error("Unsupported extended clipboard format for Provide message.");
3019 // Change lone \r or \n into \r\n as defined in rfbproto
3020 inData[i] = inData[i].replace(/\r\n|\r|\n/gm, "\r\n");
3022 // Check if it already has \0
3023 let text = encodeUTF8(inData[i] + "\0");
3025 dataToDeflate.push( (text.length >> 24) & 0xFF,
3026 (text.length >> 16) & 0xFF,
3027 (text.length >> 8) & 0xFF,
3028 (text.length & 0xFF));
3030 for (let j = 0; j < text.length; j++) {
3031 dataToDeflate.push(text.charCodeAt(j));
3035 let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate));
3037 // Build data to send
3038 let data = new Uint8Array(4 + deflatedData.length);
3039 data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide],
3041 data.set(deflatedData, 4);
3043 RFB.messages.clientCutText(sock, data, true);
3046 extendedClipboardNotify(sock, formats) {
3047 let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify],
3049 RFB.messages.clientCutText(sock, flags, true);
3052 extendedClipboardRequest(sock, formats) {
3053 let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest],
3055 RFB.messages.clientCutText(sock, flags, true);
3058 extendedClipboardCaps(sock, actions, formats) {
3059 let formatKeys = Object.keys(formats);
3060 let data = new Uint8Array(4 + (4 * formatKeys.length));
3062 formatKeys.map(x => parseInt(x));
3063 formatKeys.sort((a, b) => a - b);
3065 data.set(RFB.messages._buildExtendedClipboardFlags(actions, []));
3068 for (let i = 0; i < formatKeys.length; i++) {
3069 data[loopOffset] = formats[formatKeys[i]] >> 24;
3070 data[loopOffset + 1] = formats[formatKeys[i]] >> 16;
3071 data[loopOffset + 2] = formats[formatKeys[i]] >> 8;
3072 data[loopOffset + 3] = formats[formatKeys[i]] >> 0;
3075 data[3] |= (1 << formatKeys[i]); // Update our format flags
3078 RFB.messages.clientCutText(sock, data, true);
3081 clientCutText(sock, data, extended = false) {
3082 sock.sQpush8(6); // msg-type
3084 sock.sQpush8(0); // padding
3085 sock.sQpush8(0); // padding
3086 sock.sQpush8(0); // padding
3090 length = toUnsigned32bit(-data.length);
3092 length = data.length;
3095 sock.sQpush32(length);
3096 sock.sQpushBytes(data);
3100 setDesktopSize(sock, width, height, id, flags) {
3101 sock.sQpush8(251); // msg-type
3103 sock.sQpush8(0); // padding
3105 sock.sQpush16(width);
3106 sock.sQpush16(height);
3108 sock.sQpush8(1); // number-of-screens
3110 sock.sQpush8(0); // padding
3114 sock.sQpush16(0); // x-position
3115 sock.sQpush16(0); // y-position
3116 sock.sQpush16(width);
3117 sock.sQpush16(height);
3118 sock.sQpush32(flags);
3123 clientFence(sock, flags, payload) {
3124 sock.sQpush8(248); // msg-type
3126 sock.sQpush8(0); // padding
3127 sock.sQpush8(0); // padding
3128 sock.sQpush8(0); // padding
3130 sock.sQpush32(flags);
3132 sock.sQpush8(payload.length);
3133 sock.sQpushString(payload);
3138 enableContinuousUpdates(sock, enable, x, y, width, height) {
3139 sock.sQpush8(150); // msg-type
3141 sock.sQpush8(enable);
3145 sock.sQpush16(width);
3146 sock.sQpush16(height);
3151 pixelFormat(sock, depth, trueColor) {
3156 } else if (depth > 8) {
3162 const bits = Math.floor(depth/3);
3164 sock.sQpush8(0); // msg-type
3166 sock.sQpush8(0); // padding
3167 sock.sQpush8(0); // padding
3168 sock.sQpush8(0); // padding
3171 sock.sQpush8(depth);
3172 sock.sQpush8(0); // little-endian
3173 sock.sQpush8(trueColor ? 1 : 0);
3175 sock.sQpush16((1 << bits) - 1); // red-max
3176 sock.sQpush16((1 << bits) - 1); // green-max
3177 sock.sQpush16((1 << bits) - 1); // blue-max
3179 sock.sQpush8(bits * 0); // red-shift
3180 sock.sQpush8(bits * 1); // green-shift
3181 sock.sQpush8(bits * 2); // blue-shift
3183 sock.sQpush8(0); // padding
3184 sock.sQpush8(0); // padding
3185 sock.sQpush8(0); // padding
3190 clientEncodings(sock, encodings) {
3191 sock.sQpush8(2); // msg-type
3193 sock.sQpush8(0); // padding
3195 sock.sQpush16(encodings.length);
3196 for (let i = 0; i < encodings.length; i++) {
3197 sock.sQpush32(encodings[i]);
3203 fbUpdateRequest(sock, incremental, x, y, w, h) {
3204 if (typeof(x) === "undefined") { x = 0; }
3205 if (typeof(y) === "undefined") { y = 0; }
3207 sock.sQpush8(3); // msg-type
3209 sock.sQpush8(incremental ? 1 : 0);
3219 xvpOp(sock, ver, op) {
3220 sock.sQpush8(250); // msg-type
3222 sock.sQpush8(0); // padding
3233 rgbaPixels: new Uint8Array(),
3239 /* eslint-disable indent */
3240 rgbaPixels: new Uint8Array([
3241 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
3242 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
3243 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
3245 /* eslint-enable indent */