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.
10 import { toUnsigned32bit
, toSigned32bit
} from './util/int.js';
11 import * as Log
from './util/logging.js';
12 import { encodeUTF8
, decodeUTF8
} from './util/strings.js';
13 import { dragThreshold
} from './util/browser.js';
14 import { clientToElement
} from './util/element.js';
15 import { setCapture
} from './util/events.js';
16 import EventTargetMixin
from './util/eventtarget.js';
17 import Display
from "./display.js";
18 import Inflator
from "./inflator.js";
19 import Deflator
from "./deflator.js";
20 import Keyboard
from "./input/keyboard.js";
21 import GestureHandler
from "./input/gesturehandler.js";
22 import Cursor
from "./util/cursor.js";
23 import Websock
from "./websock.js";
24 import DES
from "./des.js";
25 import KeyTable
from "./input/keysym.js";
26 import XtScancode
from "./input/xtscancodes.js";
27 import { encodings
} from "./encodings.js";
29 import RawDecoder
from "./decoders/raw.js";
30 import CopyRectDecoder
from "./decoders/copyrect.js";
31 import RREDecoder
from "./decoders/rre.js";
32 import HextileDecoder
from "./decoders/hextile.js";
33 import TightDecoder
from "./decoders/tight.js";
34 import TightPNGDecoder
from "./decoders/tightpng.js";
36 // How many seconds to wait for a disconnect to finish
37 const DISCONNECT_TIMEOUT
= 3;
38 const DEFAULT_BACKGROUND
= 'rgb(40, 40, 40)';
40 // Minimum wait (ms) between two mouse moves
41 const MOUSE_MOVE_DELAY
= 17;
44 const WHEEL_STEP
= 50; // Pixels needed for one step
45 const WHEEL_LINE_HEIGHT
= 19; // Assumed pixels for one line step
48 const GESTURE_ZOOMSENS
= 75;
49 const GESTURE_SCRLSENS
= 50;
50 const DOUBLE_TAP_TIMEOUT
= 1000;
51 const DOUBLE_TAP_THRESHOLD
= 50;
53 // Extended clipboard pseudo-encoding formats
54 const extendedClipboardFormatText
= 1;
55 /*eslint-disable no-unused-vars */
56 const extendedClipboardFormatRtf
= 1 << 1;
57 const extendedClipboardFormatHtml
= 1 << 2;
58 const extendedClipboardFormatDib
= 1 << 3;
59 const extendedClipboardFormatFiles
= 1 << 4;
62 // Extended clipboard pseudo-encoding actions
63 const extendedClipboardActionCaps
= 1 << 24;
64 const extendedClipboardActionRequest
= 1 << 25;
65 const extendedClipboardActionPeek
= 1 << 26;
66 const extendedClipboardActionNotify
= 1 << 27;
67 const extendedClipboardActionProvide
= 1 << 28;
69 export default class RFB
extends EventTargetMixin
{
70 constructor(target
, urlOrChannel
, options
) {
72 throw new Error("Must specify target");
75 throw new Error("Must specify URL, WebSocket or RTCDataChannel");
80 this._target
= target
;
82 if (typeof urlOrChannel
=== "string") {
83 this._url
= urlOrChannel
;
86 this._rawChannel
= urlOrChannel
;
90 options
= options
|| {};
91 this._rfbCredentials
= options
.credentials
|| {};
92 this._shared
= 'shared' in options
? !!options
.shared
: true;
93 this._repeaterID
= options
.repeaterID
|| '';
94 this._wsProtocols
= options
.wsProtocols
|| [];
97 this._rfbConnectionState
= '';
98 this._rfbInitState
= '';
99 this._rfbAuthScheme
= -1;
100 this._rfbCleanDisconnect
= true;
102 // Server capabilities
103 this._rfbVersion
= 0;
104 this._rfbMaxVersion
= 3.8;
105 this._rfbTightVNC
= false;
106 this._rfbVeNCryptState
= 0;
114 this._capabilities
= { power
: false };
116 this._supportsFence
= false;
118 this._supportsContinuousUpdates
= false;
119 this._enabledContinuousUpdates
= false;
121 this._supportsSetDesktopSize
= false;
123 this._screenFlags
= 0;
125 this._qemuExtKeyEventSupported
= false;
127 this._clipboardText
= null;
128 this._clipboardServerCapabilitiesActions
= {};
129 this._clipboardServerCapabilitiesFormats
= {};
132 this._sock
= null; // Websock object
133 this._display
= null; // Display object
134 this._flushing
= false; // Display flushing state
135 this._keyboard
= null; // Keyboard input handler object
136 this._gestures
= null; // Gesture input handler object
139 this._disconnTimer
= null; // disconnection timer
140 this._resizeTimeout
= null; // resize rate limiting
141 this._mouseMoveTimer
= null;
157 this._mouseButtonMask
= 0;
158 this._mouseLastMoveTime
= 0;
159 this._viewportDragging
= false;
160 this._viewportDragPos
= {};
161 this._viewportHasMoved
= false;
162 this._accumulatedWheelDeltaX
= 0;
163 this._accumulatedWheelDeltaY
= 0;
166 this._gestureLastTapTime
= null;
167 this._gestureFirstDoubleTapEv
= null;
168 this._gestureLastMagnitudeX
= 0;
169 this._gestureLastMagnitudeY
= 0;
171 // Bound event handlers
172 this._eventHandlers
= {
173 focusCanvas
: this._focusCanvas
.bind(this),
174 windowResize
: this._windowResize
.bind(this),
175 handleMouse
: this._handleMouse
.bind(this),
176 handleWheel
: this._handleWheel
.bind(this),
177 handleGesture
: this._handleGesture
.bind(this),
181 Log
.Debug(">> RFB.constructor");
183 // Create DOM elements
184 this._screen
= document
.createElement('div');
185 this._screen
.style
.display
= 'flex';
186 this._screen
.style
.width
= '100%';
187 this._screen
.style
.height
= '100%';
188 this._screen
.style
.overflow
= 'auto';
189 this._screen
.style
.background
= DEFAULT_BACKGROUND
;
190 this._canvas
= document
.createElement('canvas');
191 this._canvas
.style
.margin
= 'auto';
192 // Some browsers add an outline on focus
193 this._canvas
.style
.outline
= 'none';
194 this._canvas
.width
= 0;
195 this._canvas
.height
= 0;
196 this._canvas
.tabIndex
= -1;
197 this._screen
.appendChild(this._canvas
);
200 this._cursor
= new Cursor();
202 // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
203 // it. Result: no cursor at all until a window border or an edit field
204 // is hit blindly. But there are also VNC servers that draw the cursor
205 // in the framebuffer and don't send the empty local cursor. There is
206 // no way to satisfy both sides.
208 // The spec is unclear on this "initial cursor" issue. Many other
209 // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
210 // initial cursor instead.
211 this._cursorImage
= RFB
.cursors
.none
;
213 // populate decoder array with objects
214 this._decoders
[encodings
.encodingRaw
] = new RawDecoder();
215 this._decoders
[encodings
.encodingCopyRect
] = new CopyRectDecoder();
216 this._decoders
[encodings
.encodingRRE
] = new RREDecoder();
217 this._decoders
[encodings
.encodingHextile
] = new HextileDecoder();
218 this._decoders
[encodings
.encodingTight
] = new TightDecoder();
219 this._decoders
[encodings
.encodingTightPNG
] = new TightPNGDecoder();
221 // NB: nothing that needs explicit teardown should be done
222 // before this point, since this can throw an exception
224 this._display
= new Display(this._canvas
);
226 Log
.Error("Display exception: " + exc
);
229 this._display
.onflush
= this._onFlush
.bind(this);
231 this._keyboard
= new Keyboard(this._canvas
);
232 this._keyboard
.onkeyevent
= this._handleKeyEvent
.bind(this);
234 this._gestures
= new GestureHandler();
236 this._sock
= new Websock();
237 this._sock
.on('open', this._socketOpen
.bind(this));
238 this._sock
.on('close', this._socketClose
.bind(this));
239 this._sock
.on('message', this._handleMessage
.bind(this));
240 this._sock
.on('error', this._socketError
.bind(this));
242 // All prepared, kick off the connection
243 this._updateConnectionState('connecting');
245 Log
.Debug("<< RFB.constructor");
247 // ===== PROPERTIES =====
249 this.dragViewport
= false;
250 this.focusOnClick
= true;
252 this._viewOnly
= false;
253 this._clipViewport
= false;
254 this._scaleViewport
= false;
255 this._resizeSession
= false;
257 this._showDotCursor
= false;
258 if (options
.showDotCursor
!== undefined) {
259 Log
.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated");
260 this._showDotCursor
= options
.showDotCursor
;
263 this._qualityLevel
= 6;
264 this._compressionLevel
= 2;
267 // ===== PROPERTIES =====
269 get viewOnly() { return this._viewOnly
; }
270 set viewOnly(viewOnly
) {
271 this._viewOnly
= viewOnly
;
273 if (this._rfbConnectionState
=== "connecting" ||
274 this._rfbConnectionState
=== "connected") {
276 this._keyboard
.ungrab();
278 this._keyboard
.grab();
283 get capabilities() { return this._capabilities
; }
285 get touchButton() { return 0; }
286 set touchButton(button
) { Log
.Warn("Using old API!"); }
288 get clipViewport() { return this._clipViewport
; }
289 set clipViewport(viewport
) {
290 this._clipViewport
= viewport
;
294 get scaleViewport() { return this._scaleViewport
; }
295 set scaleViewport(scale
) {
296 this._scaleViewport
= scale
;
297 // Scaling trumps clipping, so we may need to adjust
298 // clipping when enabling or disabling scaling
299 if (scale
&& this._clipViewport
) {
303 if (!scale
&& this._clipViewport
) {
308 get resizeSession() { return this._resizeSession
; }
309 set resizeSession(resize
) {
310 this._resizeSession
= resize
;
312 this._requestRemoteResize();
316 get showDotCursor() { return this._showDotCursor
; }
317 set showDotCursor(show
) {
318 this._showDotCursor
= show
;
319 this._refreshCursor();
322 get background() { return this._screen
.style
.background
; }
323 set background(cssValue
) { this._screen
.style
.background
= cssValue
; }
326 return this._qualityLevel
;
328 set qualityLevel(qualityLevel
) {
329 if (!Number
.isInteger(qualityLevel
) || qualityLevel
< 0 || qualityLevel
> 9) {
330 Log
.Error("qualityLevel must be an integer between 0 and 9");
334 if (this._qualityLevel
=== qualityLevel
) {
338 this._qualityLevel
= qualityLevel
;
340 if (this._rfbConnectionState
=== 'connected') {
341 this._sendEncodings();
345 get compressionLevel() {
346 return this._compressionLevel
;
348 set compressionLevel(compressionLevel
) {
349 if (!Number
.isInteger(compressionLevel
) || compressionLevel
< 0 || compressionLevel
> 9) {
350 Log
.Error("compressionLevel must be an integer between 0 and 9");
354 if (this._compressionLevel
=== compressionLevel
) {
358 this._compressionLevel
= compressionLevel
;
360 if (this._rfbConnectionState
=== 'connected') {
361 this._sendEncodings();
365 // ===== PUBLIC METHODS =====
368 this._updateConnectionState('disconnecting');
369 this._sock
.off('error');
370 this._sock
.off('message');
371 this._sock
.off('open');
374 sendCredentials(creds
) {
375 this._rfbCredentials
= creds
;
376 setTimeout(this._initMsg
.bind(this), 0);
380 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
381 Log
.Info("Sending Ctrl-Alt-Del");
383 this.sendKey(KeyTable
.XK_Control_L
, "ControlLeft", true);
384 this.sendKey(KeyTable
.XK_Alt_L
, "AltLeft", true);
385 this.sendKey(KeyTable
.XK_Delete
, "Delete", true);
386 this.sendKey(KeyTable
.XK_Delete
, "Delete", false);
387 this.sendKey(KeyTable
.XK_Alt_L
, "AltLeft", false);
388 this.sendKey(KeyTable
.XK_Control_L
, "ControlLeft", false);
403 // Send a key press. If 'down' is not specified then send a down key
404 // followed by an up key.
405 sendKey(keysym
, code
, down
) {
406 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
408 if (down
=== undefined) {
409 this.sendKey(keysym
, code
, true);
410 this.sendKey(keysym
, code
, false);
414 const scancode
= XtScancode
[code
];
416 if (this._qemuExtKeyEventSupported
&& scancode
) {
418 keysym
= keysym
|| 0;
420 Log
.Info("Sending key (" + (down
? "down" : "up") + "): keysym " + keysym
+ ", scancode " + scancode
);
422 RFB
.messages
.QEMUExtendedKeyEvent(this._sock
, keysym
, down
, scancode
);
427 Log
.Info("Sending keysym (" + (down
? "down" : "up") + "): " + keysym
);
428 RFB
.messages
.keyEvent(this._sock
, keysym
, down
? 1 : 0);
433 this._canvas
.focus();
440 clipboardPasteFrom(text
) {
441 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
443 if (this._clipboardServerCapabilitiesFormats
[extendedClipboardFormatText
] &&
444 this._clipboardServerCapabilitiesActions
[extendedClipboardActionNotify
]) {
446 this._clipboardText
= text
;
447 RFB
.messages
.extendedClipboardNotify(this._sock
, [extendedClipboardFormatText
]);
449 let data
= new Uint8Array(text
.length
);
450 for (let i
= 0; i
< text
.length
; i
++) {
451 // FIXME: text can have values outside of Latin1/Uint8
452 data
[i
] = text
.charCodeAt(i
);
455 RFB
.messages
.clientCutText(this._sock
, data
);
459 // ===== PRIVATE METHODS =====
462 Log
.Debug(">> RFB.connect");
465 Log
.Info(`connecting to ${this._url}`);
466 this._sock
.open(this._url
, this._wsProtocols
);
468 Log
.Info(`attaching ${this._rawChannel} to Websock`);
469 this._sock
.attach(this._rawChannel
);
471 if (this._sock
.readyState
=== 'closed') {
472 throw Error("Cannot use already closed WebSocket/RTCDataChannel");
475 if (this._sock
.readyState
=== 'open') {
476 // FIXME: _socketOpen() can in theory call _fail(), which
477 // isn't allowed this early, but I'm not sure that can
478 // happen without a bug messing up our state variables
483 // Make our elements part of the page
484 this._target
.appendChild(this._screen
);
486 this._gestures
.attach(this._canvas
);
488 this._cursor
.attach(this._canvas
);
489 this._refreshCursor();
491 // Monitor size changes of the screen
492 // FIXME: Use ResizeObserver, or hidden overflow
493 window
.addEventListener('resize', this._eventHandlers
.windowResize
);
495 // Always grab focus on some kind of click event
496 this._canvas
.addEventListener("mousedown", this._eventHandlers
.focusCanvas
);
497 this._canvas
.addEventListener("touchstart", this._eventHandlers
.focusCanvas
);
500 this._canvas
.addEventListener('mousedown', this._eventHandlers
.handleMouse
);
501 this._canvas
.addEventListener('mouseup', this._eventHandlers
.handleMouse
);
502 this._canvas
.addEventListener('mousemove', this._eventHandlers
.handleMouse
);
503 // Prevent middle-click pasting (see handler for why we bind to document)
504 this._canvas
.addEventListener('click', this._eventHandlers
.handleMouse
);
505 // preventDefault() on mousedown doesn't stop this event for some
506 // reason so we have to explicitly block it
507 this._canvas
.addEventListener('contextmenu', this._eventHandlers
.handleMouse
);
510 this._canvas
.addEventListener("wheel", this._eventHandlers
.handleWheel
);
513 this._canvas
.addEventListener("gesturestart", this._eventHandlers
.handleGesture
);
514 this._canvas
.addEventListener("gesturemove", this._eventHandlers
.handleGesture
);
515 this._canvas
.addEventListener("gestureend", this._eventHandlers
.handleGesture
);
517 Log
.Debug("<< RFB.connect");
521 Log
.Debug(">> RFB.disconnect");
522 this._cursor
.detach();
523 this._canvas
.removeEventListener("gesturestart", this._eventHandlers
.handleGesture
);
524 this._canvas
.removeEventListener("gesturemove", this._eventHandlers
.handleGesture
);
525 this._canvas
.removeEventListener("gestureend", this._eventHandlers
.handleGesture
);
526 this._canvas
.removeEventListener("wheel", this._eventHandlers
.handleWheel
);
527 this._canvas
.removeEventListener('mousedown', this._eventHandlers
.handleMouse
);
528 this._canvas
.removeEventListener('mouseup', this._eventHandlers
.handleMouse
);
529 this._canvas
.removeEventListener('mousemove', this._eventHandlers
.handleMouse
);
530 this._canvas
.removeEventListener('click', this._eventHandlers
.handleMouse
);
531 this._canvas
.removeEventListener('contextmenu', this._eventHandlers
.handleMouse
);
532 this._canvas
.removeEventListener("mousedown", this._eventHandlers
.focusCanvas
);
533 this._canvas
.removeEventListener("touchstart", this._eventHandlers
.focusCanvas
);
534 window
.removeEventListener('resize', this._eventHandlers
.windowResize
);
535 this._keyboard
.ungrab();
536 this._gestures
.detach();
539 this._target
.removeChild(this._screen
);
541 if (e
.name
=== 'NotFoundError') {
542 // Some cases where the initial connection fails
543 // can disconnect before the _screen is created
548 clearTimeout(this._resizeTimeout
);
549 clearTimeout(this._mouseMoveTimer
);
550 Log
.Debug("<< RFB.disconnect");
554 if ((this._rfbConnectionState
=== 'connecting') &&
555 (this._rfbInitState
=== '')) {
556 this._rfbInitState
= 'ProtocolVersion';
557 Log
.Debug("Starting VNC handshake");
559 this._fail("Unexpected server connection while " +
560 this._rfbConnectionState
);
565 Log
.Debug("WebSocket on-close event");
568 msg
= "(code: " + e
.code
;
570 msg
+= ", reason: " + e
.reason
;
574 switch (this._rfbConnectionState
) {
576 this._fail("Connection closed " + msg
);
579 // Handle disconnects that were initiated server-side
580 this._updateConnectionState('disconnecting');
581 this._updateConnectionState('disconnected');
583 case 'disconnecting':
584 // Normal disconnection path
585 this._updateConnectionState('disconnected');
588 this._fail("Unexpected server disconnect " +
589 "when already disconnected " + msg
);
592 this._fail("Unexpected server disconnect before connecting " +
596 this._sock
.off('close');
597 // Delete reference to raw channel to allow cleanup.
598 this._rawChannel
= null;
602 Log
.Warn("WebSocket on-error event");
605 _focusCanvas(event
) {
606 if (!this.focusOnClick
) {
613 _setDesktopName(name
) {
615 this.dispatchEvent(new CustomEvent(
617 { detail
: { name
: this._fbName
} }));
620 _windowResize(event
) {
621 // If the window resized then our screen element might have
622 // as well. Update the viewport dimensions.
623 window
.requestAnimationFrame(() => {
628 if (this._resizeSession
) {
629 // Request changing the resolution of the remote display to
630 // the size of the local browser viewport.
632 // In order to not send multiple requests before the browser-resize
633 // is finished we wait 0.5 seconds before sending the request.
634 clearTimeout(this._resizeTimeout
);
635 this._resizeTimeout
= setTimeout(this._requestRemoteResize
.bind(this), 500);
639 // Update state of clipping in Display object, and make sure the
640 // configured viewport matches the current screen size
642 const curClip
= this._display
.clipViewport
;
643 let newClip
= this._clipViewport
;
645 if (this._scaleViewport
) {
646 // Disable viewport clipping if we are scaling
650 if (curClip
!== newClip
) {
651 this._display
.clipViewport
= newClip
;
655 // When clipping is enabled, the screen is limited to
656 // the size of the container.
657 const size
= this._screenSize();
658 this._display
.viewportChangeSize(size
.w
, size
.h
);
659 this._fixScrollbars();
664 if (!this._scaleViewport
) {
665 this._display
.scale
= 1.0;
667 const size
= this._screenSize();
668 this._display
.autoscale(size
.w
, size
.h
);
670 this._fixScrollbars();
673 // Requests a change of remote desktop size. This message is an extension
674 // and may only be sent if we have received an ExtendedDesktopSize message
675 _requestRemoteResize() {
676 clearTimeout(this._resizeTimeout
);
677 this._resizeTimeout
= null;
679 if (!this._resizeSession
|| this._viewOnly
||
680 !this._supportsSetDesktopSize
) {
684 const size
= this._screenSize();
685 RFB
.messages
.setDesktopSize(this._sock
,
686 Math
.floor(size
.w
), Math
.floor(size
.h
),
687 this._screenID
, this._screenFlags
);
689 Log
.Debug('Requested new desktop size: ' +
690 size
.w
+ 'x' + size
.h
);
693 // Gets the the size of the available screen
695 let r
= this._screen
.getBoundingClientRect();
696 return { w
: r
.width
, h
: r
.height
};
700 // This is a hack because Chrome screws up the calculation
701 // for when scrollbars are needed. So to fix it we temporarily
702 // toggle them off and on.
703 const orig
= this._screen
.style
.overflow
;
704 this._screen
.style
.overflow
= 'hidden';
705 // Force Chrome to recalculate the layout by asking for
706 // an element's dimensions
707 this._screen
.getBoundingClientRect();
708 this._screen
.style
.overflow
= orig
;
716 * disconnected - permanent state
718 _updateConnectionState(state
) {
719 const oldstate
= this._rfbConnectionState
;
721 if (state
=== oldstate
) {
722 Log
.Debug("Already in state '" + state
+ "', ignoring");
726 // The 'disconnected' state is permanent for each RFB object
727 if (oldstate
=== 'disconnected') {
728 Log
.Error("Tried changing state of a disconnected RFB object");
732 // Ensure proper transitions before doing anything
735 if (oldstate
!== 'connecting') {
736 Log
.Error("Bad transition to connected state, " +
737 "previous connection state: " + oldstate
);
743 if (oldstate
!== 'disconnecting') {
744 Log
.Error("Bad transition to disconnected state, " +
745 "previous connection state: " + oldstate
);
751 if (oldstate
!== '') {
752 Log
.Error("Bad transition to connecting state, " +
753 "previous connection state: " + oldstate
);
758 case 'disconnecting':
759 if (oldstate
!== 'connected' && oldstate
!== 'connecting') {
760 Log
.Error("Bad transition to disconnecting state, " +
761 "previous connection state: " + oldstate
);
767 Log
.Error("Unknown connection state: " + state
);
771 // State change actions
773 this._rfbConnectionState
= state
;
775 Log
.Debug("New state '" + state
+ "', was '" + oldstate
+ "'.");
777 if (this._disconnTimer
&& state
!== 'disconnecting') {
778 Log
.Debug("Clearing disconnect timer");
779 clearTimeout(this._disconnTimer
);
780 this._disconnTimer
= null;
782 // make sure we don't get a double event
783 this._sock
.off('close');
792 this.dispatchEvent(new CustomEvent("connect", { detail
: {} }));
795 case 'disconnecting':
798 this._disconnTimer
= setTimeout(() => {
799 Log
.Error("Disconnection timed out.");
800 this._updateConnectionState('disconnected');
801 }, DISCONNECT_TIMEOUT
* 1000);
805 this.dispatchEvent(new CustomEvent(
806 "disconnect", { detail
:
807 { clean
: this._rfbCleanDisconnect
} }));
812 /* Print errors and disconnect
814 * The parameter 'details' is used for information that
815 * should be logged but not sent to the user interface.
818 switch (this._rfbConnectionState
) {
819 case 'disconnecting':
820 Log
.Error("Failed when disconnecting: " + details
);
823 Log
.Error("Failed while connected: " + details
);
826 Log
.Error("Failed when connecting: " + details
);
829 Log
.Error("RFB failure: " + details
);
832 this._rfbCleanDisconnect
= false; //This is sent to the UI
834 // Transition to disconnected without waiting for socket to close
835 this._updateConnectionState('disconnecting');
836 this._updateConnectionState('disconnected');
841 _setCapability(cap
, val
) {
842 this._capabilities
[cap
] = val
;
843 this.dispatchEvent(new CustomEvent("capabilities",
844 { detail
: { capabilities
: this._capabilities
} }));
848 if (this._sock
.rQlen
=== 0) {
849 Log
.Warn("handleMessage called on an empty receive queue");
853 switch (this._rfbConnectionState
) {
855 Log
.Error("Got data while disconnected");
859 if (this._flushing
) {
862 if (!this._normalMsg()) {
865 if (this._sock
.rQlen
=== 0) {
876 _handleKeyEvent(keysym
, code
, down
) {
877 this.sendKey(keysym
, code
, down
);
882 * We don't check connection status or viewOnly here as the
883 * mouse events might be used to control the viewport
886 if (ev
.type
=== 'click') {
888 * Note: This is only needed for the 'click' event as it fails
889 * to fire properly for the target element so we have
890 * to listen on the document element instead.
892 if (ev
.target
!== this._canvas
) {
897 // FIXME: if we're in view-only and not dragging,
898 // should we stop events?
899 ev
.stopPropagation();
902 if ((ev
.type
=== 'click') || (ev
.type
=== 'contextmenu')) {
906 let pos
= clientToElement(ev
.clientX
, ev
.clientY
,
911 setCapture(this._canvas
);
912 this._handleMouseButton(pos
.x
, pos
.y
,
913 true, 1 << ev
.button
);
916 this._handleMouseButton(pos
.x
, pos
.y
,
917 false, 1 << ev
.button
);
920 this._handleMouseMove(pos
.x
, pos
.y
);
925 _handleMouseButton(x
, y
, down
, bmask
) {
926 if (this.dragViewport
) {
927 if (down
&& !this._viewportDragging
) {
928 this._viewportDragging
= true;
929 this._viewportDragPos
= {'x': x
, 'y': y
};
930 this._viewportHasMoved
= false;
932 // Skip sending mouse events
935 this._viewportDragging
= false;
937 // If we actually performed a drag then we are done
938 // here and should not send any mouse events
939 if (this._viewportHasMoved
) {
943 // Otherwise we treat this as a mouse click event.
944 // Send the button down event here, as the button up
945 // event is sent at the end of this function.
946 this._sendMouse(x
, y
, bmask
);
950 // Flush waiting move event first
951 if (this._mouseMoveTimer
!== null) {
952 clearTimeout(this._mouseMoveTimer
);
953 this._mouseMoveTimer
= null;
954 this._sendMouse(x
, y
, this._mouseButtonMask
);
958 this._mouseButtonMask
|= bmask
;
960 this._mouseButtonMask
&= ~bmask
;
963 this._sendMouse(x
, y
, this._mouseButtonMask
);
966 _handleMouseMove(x
, y
) {
967 if (this._viewportDragging
) {
968 const deltaX
= this._viewportDragPos
.x
- x
;
969 const deltaY
= this._viewportDragPos
.y
- y
;
971 if (this._viewportHasMoved
|| (Math
.abs(deltaX
) > dragThreshold
||
972 Math
.abs(deltaY
) > dragThreshold
)) {
973 this._viewportHasMoved
= true;
975 this._viewportDragPos
= {'x': x
, 'y': y
};
976 this._display
.viewportChangePos(deltaX
, deltaY
);
979 // Skip sending mouse events
983 this._mousePos
= { 'x': x
, 'y': y
};
985 // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms
986 if (this._mouseMoveTimer
== null) {
988 const timeSinceLastMove
= Date
.now() - this._mouseLastMoveTime
;
989 if (timeSinceLastMove
> MOUSE_MOVE_DELAY
) {
990 this._sendMouse(x
, y
, this._mouseButtonMask
);
991 this._mouseLastMoveTime
= Date
.now();
993 // Too soon since the latest move, wait the remaining time
994 this._mouseMoveTimer
= setTimeout(() => {
995 this._handleDelayedMouseMove();
996 }, MOUSE_MOVE_DELAY
- timeSinceLastMove
);
1001 _handleDelayedMouseMove() {
1002 this._mouseMoveTimer
= null;
1003 this._sendMouse(this._mousePos
.x
, this._mousePos
.y
,
1004 this._mouseButtonMask
);
1005 this._mouseLastMoveTime
= Date
.now();
1008 _sendMouse(x
, y
, mask
) {
1009 if (this._rfbConnectionState
!== 'connected') { return; }
1010 if (this._viewOnly
) { return; } // View only, skip mouse events
1012 RFB
.messages
.pointerEvent(this._sock
, this._display
.absX(x
),
1013 this._display
.absY(y
), mask
);
1017 if (this._rfbConnectionState
!== 'connected') { return; }
1018 if (this._viewOnly
) { return; } // View only, skip mouse events
1020 ev
.stopPropagation();
1021 ev
.preventDefault();
1023 let pos
= clientToElement(ev
.clientX
, ev
.clientY
,
1029 // Pixel units unless it's non-zero.
1030 // Note that if deltamode is line or page won't matter since we aren't
1031 // sending the mouse wheel delta to the server anyway.
1032 // The difference between pixel and line can be important however since
1033 // we have a threshold that can be smaller than the line height.
1034 if (ev
.deltaMode
!== 0) {
1035 dX
*= WHEEL_LINE_HEIGHT
;
1036 dY
*= WHEEL_LINE_HEIGHT
;
1039 // Mouse wheel events are sent in steps over VNC. This means that the VNC
1040 // protocol can't handle a wheel event with specific distance or speed.
1041 // Therefor, if we get a lot of small mouse wheel events we combine them.
1042 this._accumulatedWheelDeltaX
+= dX
;
1043 this._accumulatedWheelDeltaY
+= dY
;
1045 // Generate a mouse wheel step event when the accumulated delta
1046 // for one of the axes is large enough.
1047 if (Math
.abs(this._accumulatedWheelDeltaX
) >= WHEEL_STEP
) {
1048 if (this._accumulatedWheelDeltaX
< 0) {
1049 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 5);
1050 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 5);
1051 } else if (this._accumulatedWheelDeltaX
> 0) {
1052 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 6);
1053 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 6);
1056 this._accumulatedWheelDeltaX
= 0;
1058 if (Math
.abs(this._accumulatedWheelDeltaY
) >= WHEEL_STEP
) {
1059 if (this._accumulatedWheelDeltaY
< 0) {
1060 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 3);
1061 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 3);
1062 } else if (this._accumulatedWheelDeltaY
> 0) {
1063 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 4);
1064 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 4);
1067 this._accumulatedWheelDeltaY
= 0;
1071 _fakeMouseMove(ev
, elementX
, elementY
) {
1072 this._handleMouseMove(elementX
, elementY
);
1073 this._cursor
.move(ev
.detail
.clientX
, ev
.detail
.clientY
);
1076 _handleTapEvent(ev
, bmask
) {
1077 let pos
= clientToElement(ev
.detail
.clientX
, ev
.detail
.clientY
,
1080 // If the user quickly taps multiple times we assume they meant to
1081 // hit the same spot, so slightly adjust coordinates
1083 if ((this._gestureLastTapTime
!== null) &&
1084 ((Date
.now() - this._gestureLastTapTime
) < DOUBLE_TAP_TIMEOUT
) &&
1085 (this._gestureFirstDoubleTapEv
.detail
.type
=== ev
.detail
.type
)) {
1086 let dx
= this._gestureFirstDoubleTapEv
.detail
.clientX
- ev
.detail
.clientX
;
1087 let dy
= this._gestureFirstDoubleTapEv
.detail
.clientY
- ev
.detail
.clientY
;
1088 let distance
= Math
.hypot(dx
, dy
);
1090 if (distance
< DOUBLE_TAP_THRESHOLD
) {
1091 pos
= clientToElement(this._gestureFirstDoubleTapEv
.detail
.clientX
,
1092 this._gestureFirstDoubleTapEv
.detail
.clientY
,
1095 this._gestureFirstDoubleTapEv
= ev
;
1098 this._gestureFirstDoubleTapEv
= ev
;
1100 this._gestureLastTapTime
= Date
.now();
1102 this._fakeMouseMove(this._gestureFirstDoubleTapEv
, pos
.x
, pos
.y
);
1103 this._handleMouseButton(pos
.x
, pos
.y
, true, bmask
);
1104 this._handleMouseButton(pos
.x
, pos
.y
, false, bmask
);
1107 _handleGesture(ev
) {
1110 let pos
= clientToElement(ev
.detail
.clientX
, ev
.detail
.clientY
,
1113 case 'gesturestart':
1114 switch (ev
.detail
.type
) {
1116 this._handleTapEvent(ev
, 0x1);
1119 this._handleTapEvent(ev
, 0x4);
1122 this._handleTapEvent(ev
, 0x2);
1125 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1126 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x1);
1129 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1130 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x4);
1134 this._gestureLastMagnitudeX
= ev
.detail
.magnitudeX
;
1135 this._gestureLastMagnitudeY
= ev
.detail
.magnitudeY
;
1136 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1139 this._gestureLastMagnitudeX
= Math
.hypot(ev
.detail
.magnitudeX
,
1140 ev
.detail
.magnitudeY
);
1141 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1147 switch (ev
.detail
.type
) {
1154 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1157 // Always scroll in the same position.
1158 // We don't know if the mouse was moved so we need to move it
1160 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1161 while ((ev
.detail
.magnitudeY
- this._gestureLastMagnitudeY
) > GESTURE_SCRLSENS
) {
1162 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x8);
1163 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x8);
1164 this._gestureLastMagnitudeY
+= GESTURE_SCRLSENS
;
1166 while ((ev
.detail
.magnitudeY
- this._gestureLastMagnitudeY
) < -GESTURE_SCRLSENS
) {
1167 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x10);
1168 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x10);
1169 this._gestureLastMagnitudeY
-= GESTURE_SCRLSENS
;
1171 while ((ev
.detail
.magnitudeX
- this._gestureLastMagnitudeX
) > GESTURE_SCRLSENS
) {
1172 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x20);
1173 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x20);
1174 this._gestureLastMagnitudeX
+= GESTURE_SCRLSENS
;
1176 while ((ev
.detail
.magnitudeX
- this._gestureLastMagnitudeX
) < -GESTURE_SCRLSENS
) {
1177 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x40);
1178 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x40);
1179 this._gestureLastMagnitudeX
-= GESTURE_SCRLSENS
;
1183 // Always scroll in the same position.
1184 // We don't know if the mouse was moved so we need to move it
1186 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1187 magnitude
= Math
.hypot(ev
.detail
.magnitudeX
, ev
.detail
.magnitudeY
);
1188 if (Math
.abs(magnitude
- this._gestureLastMagnitudeX
) > GESTURE_ZOOMSENS
) {
1189 this._handleKeyEvent(KeyTable
.XK_Control_L
, "ControlLeft", true);
1190 while ((magnitude
- this._gestureLastMagnitudeX
) > GESTURE_ZOOMSENS
) {
1191 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x8);
1192 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x8);
1193 this._gestureLastMagnitudeX
+= GESTURE_ZOOMSENS
;
1195 while ((magnitude
- this._gestureLastMagnitudeX
) < -GESTURE_ZOOMSENS
) {
1196 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x10);
1197 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x10);
1198 this._gestureLastMagnitudeX
-= GESTURE_ZOOMSENS
;
1201 this._handleKeyEvent(KeyTable
.XK_Control_L
, "ControlLeft", false);
1207 switch (ev
.detail
.type
) {
1215 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1216 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x1);
1219 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1220 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x4);
1229 _negotiateProtocolVersion() {
1230 if (this._sock
.rQwait("version", 12)) {
1234 const sversion
= this._sock
.rQshiftStr(12).substr(4, 7);
1235 Log
.Info("Server ProtocolVersion: " + sversion
);
1238 case "000.000": // UltraVNC repeater
1242 case "003.006": // UltraVNC
1243 case "003.889": // Apple Remote Desktop
1244 this._rfbVersion
= 3.3;
1247 this._rfbVersion
= 3.7;
1250 case "004.000": // Intel AMT KVM
1251 case "004.001": // RealVNC 4.6
1252 case "005.000": // RealVNC 5.3
1253 this._rfbVersion
= 3.8;
1256 return this._fail("Invalid server version " + sversion
);
1260 let repeaterID
= "ID:" + this._repeaterID
;
1261 while (repeaterID
.length
< 250) {
1264 this._sock
.sendString(repeaterID
);
1268 if (this._rfbVersion
> this._rfbMaxVersion
) {
1269 this._rfbVersion
= this._rfbMaxVersion
;
1272 const cversion
= "00" + parseInt(this._rfbVersion
, 10) +
1273 ".00" + ((this._rfbVersion
* 10) % 10);
1274 this._sock
.sendString("RFB " + cversion
+ "\n");
1275 Log
.Debug('Sent ProtocolVersion: ' + cversion
);
1277 this._rfbInitState
= 'Security';
1280 _negotiateSecurity() {
1281 if (this._rfbVersion
>= 3.7) {
1282 // Server sends supported list, client decides
1283 const numTypes
= this._sock
.rQshift8();
1284 if (this._sock
.rQwait("security type", numTypes
, 1)) { return false; }
1286 if (numTypes
=== 0) {
1287 this._rfbInitState
= "SecurityReason";
1288 this._securityContext
= "no security types";
1289 this._securityStatus
= 1;
1290 return this._initMsg();
1293 const types
= this._sock
.rQshiftBytes(numTypes
);
1294 Log
.Debug("Server security types: " + types
);
1296 // Look for each auth in preferred order
1297 if (types
.includes(1)) {
1298 this._rfbAuthScheme
= 1; // None
1299 } else if (types
.includes(22)) {
1300 this._rfbAuthScheme
= 22; // XVP
1301 } else if (types
.includes(16)) {
1302 this._rfbAuthScheme
= 16; // Tight
1303 } else if (types
.includes(2)) {
1304 this._rfbAuthScheme
= 2; // VNC Auth
1305 } else if (types
.includes(19)) {
1306 this._rfbAuthScheme
= 19; // VeNCrypt Auth
1308 return this._fail("Unsupported security types (types: " + types
+ ")");
1311 this._sock
.send([this._rfbAuthScheme
]);
1314 if (this._sock
.rQwait("security scheme", 4)) { return false; }
1315 this._rfbAuthScheme
= this._sock
.rQshift32();
1317 if (this._rfbAuthScheme
== 0) {
1318 this._rfbInitState
= "SecurityReason";
1319 this._securityContext
= "authentication scheme";
1320 this._securityStatus
= 1;
1321 return this._initMsg();
1325 this._rfbInitState
= 'Authentication';
1326 Log
.Debug('Authenticating using scheme: ' + this._rfbAuthScheme
);
1328 return this._initMsg(); // jump to authentication
1331 _handleSecurityReason() {
1332 if (this._sock
.rQwait("reason length", 4)) {
1335 const strlen
= this._sock
.rQshift32();
1339 if (this._sock
.rQwait("reason", strlen
, 4)) { return false; }
1340 reason
= this._sock
.rQshiftStr(strlen
);
1343 if (reason
!== "") {
1344 this.dispatchEvent(new CustomEvent(
1346 { detail
: { status
: this._securityStatus
,
1347 reason
: reason
} }));
1349 return this._fail("Security negotiation failed on " +
1350 this._securityContext
+
1351 " (reason: " + reason
+ ")");
1353 this.dispatchEvent(new CustomEvent(
1355 { detail
: { status
: this._securityStatus
} }));
1357 return this._fail("Security negotiation failed on " +
1358 this._securityContext
);
1363 _negotiateXvpAuth() {
1364 if (this._rfbCredentials
.username
=== undefined ||
1365 this._rfbCredentials
.password
=== undefined ||
1366 this._rfbCredentials
.target
=== undefined) {
1367 this.dispatchEvent(new CustomEvent(
1368 "credentialsrequired",
1369 { detail
: { types
: ["username", "password", "target"] } }));
1373 const xvpAuthStr
= String
.fromCharCode(this._rfbCredentials
.username
.length
) +
1374 String
.fromCharCode(this._rfbCredentials
.target
.length
) +
1375 this._rfbCredentials
.username
+
1376 this._rfbCredentials
.target
;
1377 this._sock
.sendString(xvpAuthStr
);
1378 this._rfbAuthScheme
= 2;
1379 return this._negotiateAuthentication();
1382 // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
1383 _negotiateVeNCryptAuth() {
1385 // waiting for VeNCrypt version
1386 if (this._rfbVeNCryptState
== 0) {
1387 if (this._sock
.rQwait("vencrypt version", 2)) { return false; }
1389 const major
= this._sock
.rQshift8();
1390 const minor
= this._sock
.rQshift8();
1392 if (!(major
== 0 && minor
== 2)) {
1393 return this._fail("Unsupported VeNCrypt version " + major
+ "." + minor
);
1396 this._sock
.send([0, 2]);
1397 this._rfbVeNCryptState
= 1;
1401 if (this._rfbVeNCryptState
== 1) {
1402 if (this._sock
.rQwait("vencrypt ack", 1)) { return false; }
1404 const res
= this._sock
.rQshift8();
1407 return this._fail("VeNCrypt failure " + res
);
1410 this._rfbVeNCryptState
= 2;
1412 // must fall through here (i.e. no "else if"), beacause we may have already received
1413 // the subtypes length and won't be called again
1415 if (this._rfbVeNCryptState
== 2) { // waiting for subtypes length
1416 if (this._sock
.rQwait("vencrypt subtypes length", 1)) { return false; }
1418 const subtypesLength
= this._sock
.rQshift8();
1419 if (subtypesLength
< 1) {
1420 return this._fail("VeNCrypt subtypes empty");
1423 this._rfbVeNCryptSubtypesLength
= subtypesLength
;
1424 this._rfbVeNCryptState
= 3;
1427 // waiting for subtypes list
1428 if (this._rfbVeNCryptState
== 3) {
1429 if (this._sock
.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength
)) { return false; }
1431 const subtypes
= [];
1432 for (let i
= 0; i
< this._rfbVeNCryptSubtypesLength
; i
++) {
1433 subtypes
.push(this._sock
.rQshift32());
1436 // 256 = Plain subtype
1437 if (subtypes
.indexOf(256) != -1) {
1439 this._sock
.send([0, 0, 1, 0]);
1440 this._rfbVeNCryptState
= 4;
1442 return this._fail("VeNCrypt Plain subtype not offered by server");
1446 // negotiated Plain subtype, server waits for password
1447 if (this._rfbVeNCryptState
== 4) {
1448 if (this._rfbCredentials
.username
=== undefined ||
1449 this._rfbCredentials
.password
=== undefined) {
1450 this.dispatchEvent(new CustomEvent(
1451 "credentialsrequired",
1452 { detail
: { types
: ["username", "password"] } }));
1456 const user
= encodeUTF8(this._rfbCredentials
.username
);
1457 const pass
= encodeUTF8(this._rfbCredentials
.password
);
1460 (user
.length
>> 24) & 0xFF,
1461 (user
.length
>> 16) & 0xFF,
1462 (user
.length
>> 8) & 0xFF,
1466 (pass
.length
>> 24) & 0xFF,
1467 (pass
.length
>> 16) & 0xFF,
1468 (pass
.length
>> 8) & 0xFF,
1471 this._sock
.sendString(user
);
1472 this._sock
.sendString(pass
);
1474 this._rfbInitState
= "SecurityResult";
1479 _negotiateStdVNCAuth() {
1480 if (this._sock
.rQwait("auth challenge", 16)) { return false; }
1482 if (this._rfbCredentials
.password
=== undefined) {
1483 this.dispatchEvent(new CustomEvent(
1484 "credentialsrequired",
1485 { detail
: { types
: ["password"] } }));
1489 // TODO(directxman12): make genDES not require an Array
1490 const challenge
= Array
.prototype.slice
.call(this._sock
.rQshiftBytes(16));
1491 const response
= RFB
.genDES(this._rfbCredentials
.password
, challenge
);
1492 this._sock
.send(response
);
1493 this._rfbInitState
= "SecurityResult";
1497 _negotiateTightUnixAuth() {
1498 if (this._rfbCredentials
.username
=== undefined ||
1499 this._rfbCredentials
.password
=== undefined) {
1500 this.dispatchEvent(new CustomEvent(
1501 "credentialsrequired",
1502 { detail
: { types
: ["username", "password"] } }));
1506 this._sock
.send([0, 0, 0, this._rfbCredentials
.username
.length
]);
1507 this._sock
.send([0, 0, 0, this._rfbCredentials
.password
.length
]);
1508 this._sock
.sendString(this._rfbCredentials
.username
);
1509 this._sock
.sendString(this._rfbCredentials
.password
);
1510 this._rfbInitState
= "SecurityResult";
1514 _negotiateTightTunnels(numTunnels
) {
1515 const clientSupportedTunnelTypes
= {
1516 0: { vendor
: 'TGHT', signature
: 'NOTUNNEL' }
1518 const serverSupportedTunnelTypes
= {};
1519 // receive tunnel capabilities
1520 for (let i
= 0; i
< numTunnels
; i
++) {
1521 const capCode
= this._sock
.rQshift32();
1522 const capVendor
= this._sock
.rQshiftStr(4);
1523 const capSignature
= this._sock
.rQshiftStr(8);
1524 serverSupportedTunnelTypes
[capCode
] = { vendor
: capVendor
, signature
: capSignature
};
1527 Log
.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes
);
1529 // Siemens touch panels have a VNC server that supports NOTUNNEL,
1530 // but forgets to advertise it. Try to detect such servers by
1531 // looking for their custom tunnel type.
1532 if (serverSupportedTunnelTypes
[1] &&
1533 (serverSupportedTunnelTypes
[1].vendor
=== "SICR") &&
1534 (serverSupportedTunnelTypes
[1].signature
=== "SCHANNEL")) {
1535 Log
.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
1536 serverSupportedTunnelTypes
[0] = { vendor
: 'TGHT', signature
: 'NOTUNNEL' };
1539 // choose the notunnel type
1540 if (serverSupportedTunnelTypes
[0]) {
1541 if (serverSupportedTunnelTypes
[0].vendor
!= clientSupportedTunnelTypes
[0].vendor
||
1542 serverSupportedTunnelTypes
[0].signature
!= clientSupportedTunnelTypes
[0].signature
) {
1543 return this._fail("Client's tunnel type had the incorrect " +
1544 "vendor or signature");
1546 Log
.Debug("Selected tunnel type: " + clientSupportedTunnelTypes
[0]);
1547 this._sock
.send([0, 0, 0, 0]); // use NOTUNNEL
1548 return false; // wait until we receive the sub auth count to continue
1550 return this._fail("Server wanted tunnels, but doesn't support " +
1551 "the notunnel type");
1555 _negotiateTightAuth() {
1556 if (!this._rfbTightVNC
) { // first pass, do the tunnel negotiation
1557 if (this._sock
.rQwait("num tunnels", 4)) { return false; }
1558 const numTunnels
= this._sock
.rQshift32();
1559 if (numTunnels
> 0 && this._sock
.rQwait("tunnel capabilities", 16 * numTunnels
, 4)) { return false; }
1561 this._rfbTightVNC
= true;
1563 if (numTunnels
> 0) {
1564 this._negotiateTightTunnels(numTunnels
);
1565 return false; // wait until we receive the sub auth to continue
1569 // second pass, do the sub-auth negotiation
1570 if (this._sock
.rQwait("sub auth count", 4)) { return false; }
1571 const subAuthCount
= this._sock
.rQshift32();
1572 if (subAuthCount
=== 0) { // empty sub-auth list received means 'no auth' subtype selected
1573 this._rfbInitState
= 'SecurityResult';
1577 if (this._sock
.rQwait("sub auth capabilities", 16 * subAuthCount
, 4)) { return false; }
1579 const clientSupportedTypes
= {
1585 const serverSupportedTypes
= [];
1587 for (let i
= 0; i
< subAuthCount
; i
++) {
1588 this._sock
.rQshift32(); // capNum
1589 const capabilities
= this._sock
.rQshiftStr(12);
1590 serverSupportedTypes
.push(capabilities
);
1593 Log
.Debug("Server Tight authentication types: " + serverSupportedTypes
);
1595 for (let authType
in clientSupportedTypes
) {
1596 if (serverSupportedTypes
.indexOf(authType
) != -1) {
1597 this._sock
.send([0, 0, 0, clientSupportedTypes
[authType
]]);
1598 Log
.Debug("Selected authentication type: " + authType
);
1601 case 'STDVNOAUTH__': // no auth
1602 this._rfbInitState
= 'SecurityResult';
1604 case 'STDVVNCAUTH_': // VNC auth
1605 this._rfbAuthScheme
= 2;
1606 return this._initMsg();
1607 case 'TGHTULGNAUTH': // UNIX auth
1608 this._rfbAuthScheme
= 129;
1609 return this._initMsg();
1611 return this._fail("Unsupported tiny auth scheme " +
1612 "(scheme: " + authType
+ ")");
1617 return this._fail("No supported sub-auth types!");
1620 _negotiateAuthentication() {
1621 switch (this._rfbAuthScheme
) {
1623 if (this._rfbVersion
>= 3.8) {
1624 this._rfbInitState
= 'SecurityResult';
1627 this._rfbInitState
= 'ClientInitialisation';
1628 return this._initMsg();
1630 case 22: // XVP auth
1631 return this._negotiateXvpAuth();
1633 case 2: // VNC authentication
1634 return this._negotiateStdVNCAuth();
1636 case 16: // TightVNC Security Type
1637 return this._negotiateTightAuth();
1639 case 19: // VeNCrypt Security Type
1640 return this._negotiateVeNCryptAuth();
1642 case 129: // TightVNC UNIX Security Type
1643 return this._negotiateTightUnixAuth();
1646 return this._fail("Unsupported auth scheme (scheme: " +
1647 this._rfbAuthScheme
+ ")");
1651 _handleSecurityResult() {
1652 if (this._sock
.rQwait('VNC auth response ', 4)) { return false; }
1654 const status
= this._sock
.rQshift32();
1656 if (status
=== 0) { // OK
1657 this._rfbInitState
= 'ClientInitialisation';
1658 Log
.Debug('Authentication OK');
1659 return this._initMsg();
1661 if (this._rfbVersion
>= 3.8) {
1662 this._rfbInitState
= "SecurityReason";
1663 this._securityContext
= "security result";
1664 this._securityStatus
= status
;
1665 return this._initMsg();
1667 this.dispatchEvent(new CustomEvent(
1669 { detail
: { status
: status
} }));
1671 return this._fail("Security handshake failed");
1676 _negotiateServerInit() {
1677 if (this._sock
.rQwait("server initialization", 24)) { return false; }
1680 const width
= this._sock
.rQshift16();
1681 const height
= this._sock
.rQshift16();
1684 const bpp
= this._sock
.rQshift8();
1685 const depth
= this._sock
.rQshift8();
1686 const bigEndian
= this._sock
.rQshift8();
1687 const trueColor
= this._sock
.rQshift8();
1689 const redMax
= this._sock
.rQshift16();
1690 const greenMax
= this._sock
.rQshift16();
1691 const blueMax
= this._sock
.rQshift16();
1692 const redShift
= this._sock
.rQshift8();
1693 const greenShift
= this._sock
.rQshift8();
1694 const blueShift
= this._sock
.rQshift8();
1695 this._sock
.rQskipBytes(3); // padding
1697 // NB(directxman12): we don't want to call any callbacks or print messages until
1698 // *after* we're past the point where we could backtrack
1700 /* Connection name/title */
1701 const nameLength = this._sock.rQshift32();
1702 if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
1703 let name = this._sock.rQshiftStr(nameLength);
1704 name = decodeUTF8(name, true);
1706 if (this._rfbTightVNC) {
1707 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
1708 // In TightVNC mode, ServerInit message is extended
1709 const numServerMessages = this._sock.rQshift16();
1710 const numClientMessages = this._sock.rQshift16();
1711 const numEncodings = this._sock.rQshift16();
1712 this._sock.rQskipBytes(2); // padding
1714 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
1715 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
1717 // we don't actually do anything with the capability information that TIGHT sends,
1718 // so we just skip the all of this.
1720 // TIGHT server message capabilities
1721 this._sock.rQskipBytes(16 * numServerMessages);
1723 // TIGHT client message capabilities
1724 this._sock.rQskipBytes(16 * numClientMessages);
1726 // TIGHT encoding capabilities
1727 this._sock.rQskipBytes(16 * numEncodings);
1730 // NB(directxman12): these are down here so that we don't run them multiple times
1732 Log.Info("Screen: " + width + "x" + height +
1733 ", bpp: " + bpp + ", depth: " + depth +
1734 ", bigEndian: " + bigEndian +
1735 ", trueColor: " + trueColor +
1736 ", redMax: " + redMax +
1737 ", greenMax: " + greenMax +
1738 ", blueMax: " + blueMax +
1739 ", redShift: " + redShift +
1740 ", greenShift: " + greenShift +
1741 ", blueShift: " + blueShift);
1743 // we're past the point where we could backtrack, so it's safe to call this
1744 this._setDesktopName(name);
1745 this._resize(width, height);
1747 if (!this._viewOnly) { this._keyboard.grab(); }
1751 if (this._fbName === "Intel(r) AMT KVM") {
1752 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
1756 RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
1757 this._sendEncodings();
1758 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
1760 this._updateConnectionState('connected');
1767 // In preference order
1768 encs.push(encodings.encodingCopyRect);
1769 // Only supported with full depth support
1770 if (this._fbDepth == 24) {
1771 encs.push(encodings.encodingTight);
1772 encs.push(encodings.encodingTightPNG);
1773 encs.push(encodings.encodingHextile);
1774 encs.push(encodings.encodingRRE);
1776 encs.push(encodings.encodingRaw);
1778 // Psuedo-encoding settings
1779 encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
1780 encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
1782 encs.push(encodings.pseudoEncodingDesktopSize);
1783 encs.push(encodings.pseudoEncodingLastRect);
1784 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
1785 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
1786 encs.push(encodings.pseudoEncodingXvp);
1787 encs.push(encodings.pseudoEncodingFence);
1788 encs.push(encodings.pseudoEncodingContinuousUpdates);
1789 encs.push(encodings.pseudoEncodingDesktopName);
1790 encs.push(encodings.pseudoEncodingExtendedClipboard);
1792 if (this._fbDepth == 24) {
1793 encs.push(encodings.pseudoEncodingVMwareCursor);
1794 encs.push(encodings.pseudoEncodingCursor);
1797 RFB.messages.clientEncodings(this._sock, encs);
1800 /* RFB protocol initialization states:
1805 * ClientInitialization - not triggered by server message
1806 * ServerInitialization
1809 switch (this._rfbInitState
) {
1810 case 'ProtocolVersion':
1811 return this._negotiateProtocolVersion();
1814 return this._negotiateSecurity();
1816 case 'Authentication':
1817 return this._negotiateAuthentication();
1819 case 'SecurityResult':
1820 return this._handleSecurityResult();
1822 case 'SecurityReason':
1823 return this._handleSecurityReason();
1825 case 'ClientInitialisation':
1826 this._sock
.send([this._shared
? 1 : 0]); // ClientInitialisation
1827 this._rfbInitState
= 'ServerInitialisation';
1830 case 'ServerInitialisation':
1831 return this._negotiateServerInit();
1834 return this._fail("Unknown init state (state: " +
1835 this._rfbInitState
+ ")");
1839 _handleSetColourMapMsg() {
1840 Log
.Debug("SetColorMapEntries");
1842 return this._fail("Unexpected SetColorMapEntries message");
1845 _handleServerCutText() {
1846 Log
.Debug("ServerCutText");
1848 if (this._sock
.rQwait("ServerCutText header", 7, 1)) { return false; }
1850 this._sock
.rQskipBytes(3); // Padding
1852 let length
= this._sock
.rQshift32();
1853 length
= toSigned32bit(length
);
1855 if (this._sock
.rQwait("ServerCutText content", Math
.abs(length
), 8)) { return false; }
1859 const text
= this._sock
.rQshiftStr(length
);
1860 if (this._viewOnly
) {
1864 this.dispatchEvent(new CustomEvent(
1866 { detail
: { text
: text
} }));
1870 length
= Math
.abs(length
);
1871 const flags
= this._sock
.rQshift32();
1872 let formats
= flags
& 0x0000FFFF;
1873 let actions
= flags
& 0xFF000000;
1875 let isCaps
= (!!(actions
& extendedClipboardActionCaps
));
1877 this._clipboardServerCapabilitiesFormats
= {};
1878 this._clipboardServerCapabilitiesActions
= {};
1880 // Update our server capabilities for Formats
1881 for (let i
= 0; i
<= 15; i
++) {
1884 // Check if format flag is set.
1885 if ((formats
& index
)) {
1886 this._clipboardServerCapabilitiesFormats
[index
] = true;
1887 // We don't send unsolicited clipboard, so we
1889 this._sock
.rQshift32();
1893 // Update our server capabilities for Actions
1894 for (let i
= 24; i
<= 31; i
++) {
1896 this._clipboardServerCapabilitiesActions
[index
] = !!(actions
& index
);
1899 /* Caps handling done, send caps with the clients
1900 capabilities set as a response */
1901 let clientActions
= [
1902 extendedClipboardActionCaps
,
1903 extendedClipboardActionRequest
,
1904 extendedClipboardActionPeek
,
1905 extendedClipboardActionNotify
,
1906 extendedClipboardActionProvide
1908 RFB
.messages
.extendedClipboardCaps(this._sock
, clientActions
, {extendedClipboardFormatText
: 0});
1910 } else if (actions
=== extendedClipboardActionRequest
) {
1911 if (this._viewOnly
) {
1915 // Check if server has told us it can handle Provide and there is clipboard data to send.
1916 if (this._clipboardText
!= null &&
1917 this._clipboardServerCapabilitiesActions
[extendedClipboardActionProvide
]) {
1919 if (formats
& extendedClipboardFormatText
) {
1920 RFB
.messages
.extendedClipboardProvide(this._sock
, [extendedClipboardFormatText
], [this._clipboardText
]);
1924 } else if (actions
=== extendedClipboardActionPeek
) {
1925 if (this._viewOnly
) {
1929 if (this._clipboardServerCapabilitiesActions
[extendedClipboardActionNotify
]) {
1931 if (this._clipboardText
!= null) {
1932 RFB
.messages
.extendedClipboardNotify(this._sock
, [extendedClipboardFormatText
]);
1934 RFB
.messages
.extendedClipboardNotify(this._sock
, []);
1938 } else if (actions
=== extendedClipboardActionNotify
) {
1939 if (this._viewOnly
) {
1943 if (this._clipboardServerCapabilitiesActions
[extendedClipboardActionRequest
]) {
1945 if (formats
& extendedClipboardFormatText
) {
1946 RFB
.messages
.extendedClipboardRequest(this._sock
, [extendedClipboardFormatText
]);
1950 } else if (actions
=== extendedClipboardActionProvide
) {
1951 if (this._viewOnly
) {
1955 if (!(formats
& extendedClipboardFormatText
)) {
1958 // Ignore what we had in our clipboard client side.
1959 this._clipboardText
= null;
1961 // FIXME: Should probably verify that this data was actually requested
1962 let zlibStream
= this._sock
.rQshiftBytes(length
- 4);
1963 let streamInflator
= new Inflator();
1964 let textData
= null;
1966 streamInflator
.setInput(zlibStream
);
1967 for (let i
= 0; i
<= 15; i
++) {
1968 let format
= 1 << i
;
1970 if (formats
& format
) {
1973 let sizeArray
= streamInflator
.inflate(4);
1975 size
|= (sizeArray
[0] << 24);
1976 size
|= (sizeArray
[1] << 16);
1977 size
|= (sizeArray
[2] << 8);
1978 size
|= (sizeArray
[3]);
1979 let chunk
= streamInflator
.inflate(size
);
1981 if (format
=== extendedClipboardFormatText
) {
1986 streamInflator
.setInput(null);
1988 if (textData
!== null) {
1990 for (let i
= 0; i
< textData
.length
; i
++) {
1991 tmpText
+= String
.fromCharCode(textData
[i
]);
1995 textData
= decodeUTF8(textData
);
1996 if ((textData
.length
> 0) && "\0" === textData
.charAt(textData
.length
- 1)) {
1997 textData
= textData
.slice(0, -1);
2000 textData
= textData
.replace("\r\n", "\n");
2002 this.dispatchEvent(new CustomEvent(
2004 { detail
: { text
: textData
} }));
2007 return this._fail("Unexpected action in extended clipboard message: " + actions
);
2013 _handleServerFenceMsg() {
2014 if (this._sock
.rQwait("ServerFence header", 8, 1)) { return false; }
2015 this._sock
.rQskipBytes(3); // Padding
2016 let flags
= this._sock
.rQshift32();
2017 let length
= this._sock
.rQshift8();
2019 if (this._sock
.rQwait("ServerFence payload", length
, 9)) { return false; }
2022 Log
.Warn("Bad payload length (" + length
+ ") in fence response");
2026 const payload
= this._sock
.rQshiftStr(length
);
2028 this._supportsFence
= true;
2033 * (1<<0) - BlockBefore
2034 * (1<<1) - BlockAfter
2039 if (!(flags
& (1<<31))) {
2040 return this._fail("Unexpected fence response");
2043 // Filter out unsupported flags
2044 // FIXME: support syncNext
2045 flags
&= (1<<0) | (1<<1);
2047 // BlockBefore and BlockAfter are automatically handled by
2048 // the fact that we process each incoming message
2050 RFB
.messages
.clientFence(this._sock
, flags
, payload
);
2056 if (this._sock
.rQwait("XVP version and message", 3, 1)) { return false; }
2057 this._sock
.rQskipBytes(1); // Padding
2058 const xvpVer
= this._sock
.rQshift8();
2059 const xvpMsg
= this._sock
.rQshift8();
2063 Log
.Error("XVP Operation Failed");
2066 this._rfbXvpVer
= xvpVer
;
2067 Log
.Info("XVP extensions enabled (version " + this._rfbXvpVer
+ ")");
2068 this._setCapability("power", true);
2071 this._fail("Illegal server XVP message (msg: " + xvpMsg
+ ")");
2080 if (this._FBU
.rects
> 0) {
2083 msgType
= this._sock
.rQshift8();
2088 case 0: // FramebufferUpdate
2089 ret
= this._framebufferUpdate();
2090 if (ret
&& !this._enabledContinuousUpdates
) {
2091 RFB
.messages
.fbUpdateRequest(this._sock
, true, 0, 0,
2092 this._fbWidth
, this._fbHeight
);
2096 case 1: // SetColorMapEntries
2097 return this._handleSetColourMapMsg();
2101 this.dispatchEvent(new CustomEvent(
2106 case 3: // ServerCutText
2107 return this._handleServerCutText();
2109 case 150: // EndOfContinuousUpdates
2110 first
= !this._supportsContinuousUpdates
;
2111 this._supportsContinuousUpdates
= true;
2112 this._enabledContinuousUpdates
= false;
2114 this._enabledContinuousUpdates
= true;
2115 this._updateContinuousUpdates();
2116 Log
.Info("Enabling continuous updates.");
2118 // FIXME: We need to send a framebufferupdaterequest here
2119 // if we add support for turning off continuous updates
2123 case 248: // ServerFence
2124 return this._handleServerFenceMsg();
2127 return this._handleXvpMsg();
2130 this._fail("Unexpected server message (type " + msgType
+ ")");
2131 Log
.Debug("sock.rQslice(0, 30): " + this._sock
.rQslice(0, 30));
2137 this._flushing
= false;
2138 // Resume processing
2139 if (this._sock
.rQlen
> 0) {
2140 this._handleMessage();
2144 _framebufferUpdate() {
2145 if (this._FBU
.rects
=== 0) {
2146 if (this._sock
.rQwait("FBU header", 3, 1)) { return false; }
2147 this._sock
.rQskipBytes(1); // Padding
2148 this._FBU
.rects
= this._sock
.rQshift16();
2150 // Make sure the previous frame is fully rendered first
2151 // to avoid building up an excessive queue
2152 if (this._display
.pending()) {
2153 this._flushing
= true;
2154 this._display
.flush();
2159 while (this._FBU
.rects
> 0) {
2160 if (this._FBU
.encoding
=== null) {
2161 if (this._sock
.rQwait("rect header", 12)) { return false; }
2162 /* New FramebufferUpdate */
2164 const hdr
= this._sock
.rQshiftBytes(12);
2165 this._FBU
.x
= (hdr
[0] << 8) + hdr
[1];
2166 this._FBU
.y
= (hdr
[2] << 8) + hdr
[3];
2167 this._FBU
.width
= (hdr
[4] << 8) + hdr
[5];
2168 this._FBU
.height
= (hdr
[6] << 8) + hdr
[7];
2169 this._FBU
.encoding
= parseInt((hdr
[8] << 24) + (hdr
[9] << 16) +
2170 (hdr
[10] << 8) + hdr
[11], 10);
2173 if (!this._handleRect()) {
2178 this._FBU
.encoding
= null;
2181 this._display
.flip();
2183 return true; // We finished this FBU
2187 switch (this._FBU
.encoding
) {
2188 case encodings
.pseudoEncodingLastRect
:
2189 this._FBU
.rects
= 1; // Will be decreased when we return
2192 case encodings
.pseudoEncodingVMwareCursor
:
2193 return this._handleVMwareCursor();
2195 case encodings
.pseudoEncodingCursor
:
2196 return this._handleCursor();
2198 case encodings
.pseudoEncodingQEMUExtendedKeyEvent
:
2199 this._qemuExtKeyEventSupported
= true;
2202 case encodings
.pseudoEncodingDesktopName
:
2203 return this._handleDesktopName();
2205 case encodings
.pseudoEncodingDesktopSize
:
2206 this._resize(this._FBU
.width
, this._FBU
.height
);
2209 case encodings
.pseudoEncodingExtendedDesktopSize
:
2210 return this._handleExtendedDesktopSize();
2213 return this._handleDataRect();
2217 _handleVMwareCursor() {
2218 const hotx
= this._FBU
.x
; // hotspot-x
2219 const hoty
= this._FBU
.y
; // hotspot-y
2220 const w
= this._FBU
.width
;
2221 const h
= this._FBU
.height
;
2222 if (this._sock
.rQwait("VMware cursor encoding", 1)) {
2226 const cursorType
= this._sock
.rQshift8();
2228 this._sock
.rQshift8(); //Padding
2231 const bytesPerPixel
= 4;
2234 if (cursorType
== 0) {
2235 //Used to filter away unimportant bits.
2236 //OR is used for correct conversion in js.
2237 const PIXEL_MASK
= 0xffffff00 | 0;
2238 rgba
= new Array(w
* h
* bytesPerPixel
);
2240 if (this._sock
.rQwait("VMware cursor classic encoding",
2241 (w
* h
* bytesPerPixel
) * 2, 2)) {
2245 let andMask
= new Array(w
* h
);
2246 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2247 andMask
[pixel
] = this._sock
.rQshift32();
2250 let xorMask
= new Array(w
* h
);
2251 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2252 xorMask
[pixel
] = this._sock
.rQshift32();
2255 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2256 if (andMask
[pixel
] == 0) {
2257 //Fully opaque pixel
2258 let bgr
= xorMask
[pixel
];
2259 let r
= bgr
>> 8 & 0xff;
2260 let g
= bgr
>> 16 & 0xff;
2261 let b
= bgr
>> 24 & 0xff;
2263 rgba
[(pixel
* bytesPerPixel
) ] = r
; //r
2264 rgba
[(pixel
* bytesPerPixel
) + 1 ] = g
; //g
2265 rgba
[(pixel
* bytesPerPixel
) + 2 ] = b
; //b
2266 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff; //a
2268 } else if ((andMask
[pixel
] & PIXEL_MASK
) ==
2270 //Only screen value matters, no mouse colouring
2271 if (xorMask
[pixel
] == 0) {
2273 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2274 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2275 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2276 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0x00;
2278 } else if ((xorMask
[pixel
] & PIXEL_MASK
) ==
2280 //Inverted pixel, not supported in browsers.
2281 //Fully opaque instead.
2282 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2283 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2284 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2285 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2289 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2290 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2291 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2292 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2297 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2298 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2299 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2300 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2305 } else if (cursorType
== 1) {
2306 if (this._sock
.rQwait("VMware cursor alpha encoding",
2311 rgba
= new Array(w
* h
* bytesPerPixel
);
2313 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2314 let data
= this._sock
.rQshift32();
2316 rgba
[(pixel
* 4) ] = data
>> 24 & 0xff; //r
2317 rgba
[(pixel
* 4) + 1 ] = data
>> 16 & 0xff; //g
2318 rgba
[(pixel
* 4) + 2 ] = data
>> 8 & 0xff; //b
2319 rgba
[(pixel
* 4) + 3 ] = data
& 0xff; //a
2323 Log
.Warn("The given cursor type is not supported: "
2324 + cursorType
+ " given.");
2328 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
2334 const hotx
= this._FBU
.x
; // hotspot-x
2335 const hoty
= this._FBU
.y
; // hotspot-y
2336 const w
= this._FBU
.width
;
2337 const h
= this._FBU
.height
;
2339 const pixelslength
= w
* h
* 4;
2340 const masklength
= Math
.ceil(w
/ 8) * h
;
2342 let bytes
= pixelslength
+ masklength
;
2343 if (this._sock
.rQwait("cursor encoding", bytes
)) {
2347 // Decode from BGRX pixels + bit mask to RGBA
2348 const pixels
= this._sock
.rQshiftBytes(pixelslength
);
2349 const mask
= this._sock
.rQshiftBytes(masklength
);
2350 let rgba
= new Uint8Array(w
* h
* 4);
2353 for (let y
= 0; y
< h
; y
++) {
2354 for (let x
= 0; x
< w
; x
++) {
2355 let maskIdx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/ 8);
2356 let alpha
= (mask
[maskIdx
] << (x
% 8)) & 0x80 ? 255 : 0;
2357 rgba
[pixIdx
] = pixels
[pixIdx
+ 2];
2358 rgba
[pixIdx
+ 1] = pixels
[pixIdx
+ 1];
2359 rgba
[pixIdx
+ 2] = pixels
[pixIdx
];
2360 rgba
[pixIdx
+ 3] = alpha
;
2365 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
2370 _handleDesktopName() {
2371 if (this._sock
.rQwait("DesktopName", 4)) {
2375 let length
= this._sock
.rQshift32();
2377 if (this._sock
.rQwait("DesktopName", length
, 4)) {
2381 let name
= this._sock
.rQshiftStr(length
);
2382 name
= decodeUTF8(name
, true);
2384 this._setDesktopName(name
);
2389 _handleExtendedDesktopSize() {
2390 if (this._sock
.rQwait("ExtendedDesktopSize", 4)) {
2394 const numberOfScreens
= this._sock
.rQpeek8();
2396 let bytes
= 4 + (numberOfScreens
* 16);
2397 if (this._sock
.rQwait("ExtendedDesktopSize", bytes
)) {
2401 const firstUpdate
= !this._supportsSetDesktopSize
;
2402 this._supportsSetDesktopSize
= true;
2404 // Normally we only apply the current resize mode after a
2405 // window resize event. However there is no such trigger on the
2406 // initial connect. And we don't know if the server supports
2407 // resizing until we've gotten here.
2409 this._requestRemoteResize();
2412 this._sock
.rQskipBytes(1); // number-of-screens
2413 this._sock
.rQskipBytes(3); // padding
2415 for (let i
= 0; i
< numberOfScreens
; i
+= 1) {
2416 // Save the id and flags of the first screen
2418 this._screenID
= this._sock
.rQshiftBytes(4); // id
2419 this._sock
.rQskipBytes(2); // x-position
2420 this._sock
.rQskipBytes(2); // y-position
2421 this._sock
.rQskipBytes(2); // width
2422 this._sock
.rQskipBytes(2); // height
2423 this._screenFlags
= this._sock
.rQshiftBytes(4); // flags
2425 this._sock
.rQskipBytes(16);
2430 * The x-position indicates the reason for the change:
2432 * 0 - server resized on its own
2433 * 1 - this client requested the resize
2434 * 2 - another client requested the resize
2437 // We need to handle errors when we requested the resize.
2438 if (this._FBU
.x
=== 1 && this._FBU
.y
!== 0) {
2440 // The y-position indicates the status code from the server
2441 switch (this._FBU
.y
) {
2443 msg
= "Resize is administratively prohibited";
2446 msg
= "Out of resources";
2449 msg
= "Invalid screen layout";
2452 msg
= "Unknown reason";
2455 Log
.Warn("Server did not accept the resize request: "
2458 this._resize(this._FBU
.width
, this._FBU
.height
);
2465 let decoder
= this._decoders
[this._FBU
.encoding
];
2467 this._fail("Unsupported encoding (encoding: " +
2468 this._FBU
.encoding
+ ")");
2473 return decoder
.decodeRect(this._FBU
.x
, this._FBU
.y
,
2474 this._FBU
.width
, this._FBU
.height
,
2475 this._sock
, this._display
,
2478 this._fail("Error decoding rect: " + err
);
2483 _updateContinuousUpdates() {
2484 if (!this._enabledContinuousUpdates
) { return; }
2486 RFB
.messages
.enableContinuousUpdates(this._sock
, true, 0, 0,
2487 this._fbWidth
, this._fbHeight
);
2490 _resize(width
, height
) {
2491 this._fbWidth
= width
;
2492 this._fbHeight
= height
;
2494 this._display
.resize(this._fbWidth
, this._fbHeight
);
2496 // Adjust the visible viewport based on the new dimensions
2498 this._updateScale();
2500 this._updateContinuousUpdates();
2504 if (this._rfbXvpVer
< ver
) { return; }
2505 Log
.Info("Sending XVP operation " + op
+ " (version " + ver
+ ")");
2506 RFB
.messages
.xvpOp(this._sock
, ver
, op
);
2509 _updateCursor(rgba
, hotx
, hoty
, w
, h
) {
2510 this._cursorImage
= {
2512 hotx
: hotx
, hoty
: hoty
, w
: w
, h
: h
,
2514 this._refreshCursor();
2517 _shouldShowDotCursor() {
2518 // Called when this._cursorImage is updated
2519 if (!this._showDotCursor
) {
2520 // User does not want to see the dot, so...
2524 // The dot should not be shown if the cursor is already visible,
2525 // i.e. contains at least one not-fully-transparent pixel.
2526 // So iterate through all alpha bytes in rgba and stop at the
2528 for (let i
= 3; i
< this._cursorImage
.rgbaPixels
.length
; i
+= 4) {
2529 if (this._cursorImage
.rgbaPixels
[i
]) {
2534 // At this point, we know that the cursor is fully transparent, and
2535 // the user wants to see the dot instead of this.
2540 if (this._rfbConnectionState
!== "connecting" &&
2541 this._rfbConnectionState
!== "connected") {
2544 const image
= this._shouldShowDotCursor() ? RFB
.cursors
.dot
: this._cursorImage
;
2545 this._cursor
.change(image
.rgbaPixels
,
2546 image
.hotx
, image
.hoty
,
2551 static genDES(password
, challenge
) {
2552 const passwordChars
= password
.split('').map(c
=> c
.charCodeAt(0));
2553 return (new DES(passwordChars
)).encrypt(challenge
);
2559 keyEvent(sock
, keysym
, down
) {
2560 const buff
= sock
._sQ
;
2561 const offset
= sock
._sQlen
;
2563 buff
[offset
] = 4; // msg-type
2564 buff
[offset
+ 1] = down
;
2566 buff
[offset
+ 2] = 0;
2567 buff
[offset
+ 3] = 0;
2569 buff
[offset
+ 4] = (keysym
>> 24);
2570 buff
[offset
+ 5] = (keysym
>> 16);
2571 buff
[offset
+ 6] = (keysym
>> 8);
2572 buff
[offset
+ 7] = keysym
;
2578 QEMUExtendedKeyEvent(sock
, keysym
, down
, keycode
) {
2579 function getRFBkeycode(xtScanCode
) {
2580 const upperByte
= (keycode
>> 8);
2581 const lowerByte
= (keycode
& 0x00ff);
2582 if (upperByte
=== 0xe0 && lowerByte
< 0x7f) {
2583 return lowerByte
| 0x80;
2588 const buff
= sock
._sQ
;
2589 const offset
= sock
._sQlen
;
2591 buff
[offset
] = 255; // msg-type
2592 buff
[offset
+ 1] = 0; // sub msg-type
2594 buff
[offset
+ 2] = (down
>> 8);
2595 buff
[offset
+ 3] = down
;
2597 buff
[offset
+ 4] = (keysym
>> 24);
2598 buff
[offset
+ 5] = (keysym
>> 16);
2599 buff
[offset
+ 6] = (keysym
>> 8);
2600 buff
[offset
+ 7] = keysym
;
2602 const RFBkeycode
= getRFBkeycode(keycode
);
2604 buff
[offset
+ 8] = (RFBkeycode
>> 24);
2605 buff
[offset
+ 9] = (RFBkeycode
>> 16);
2606 buff
[offset
+ 10] = (RFBkeycode
>> 8);
2607 buff
[offset
+ 11] = RFBkeycode
;
2613 pointerEvent(sock
, x
, y
, mask
) {
2614 const buff
= sock
._sQ
;
2615 const offset
= sock
._sQlen
;
2617 buff
[offset
] = 5; // msg-type
2619 buff
[offset
+ 1] = mask
;
2621 buff
[offset
+ 2] = x
>> 8;
2622 buff
[offset
+ 3] = x
;
2624 buff
[offset
+ 4] = y
>> 8;
2625 buff
[offset
+ 5] = y
;
2631 // Used to build Notify and Request data.
2632 _buildExtendedClipboardFlags(actions
, formats
) {
2633 let data
= new Uint8Array(4);
2634 let formatFlag
= 0x00000000;
2635 let actionFlag
= 0x00000000;
2637 for (let i
= 0; i
< actions
.length
; i
++) {
2638 actionFlag
|= actions
[i
];
2641 for (let i
= 0; i
< formats
.length
; i
++) {
2642 formatFlag
|= formats
[i
];
2645 data
[0] = actionFlag
>> 24; // Actions
2646 data
[1] = 0x00; // Reserved
2647 data
[2] = 0x00; // Reserved
2648 data
[3] = formatFlag
; // Formats
2653 extendedClipboardProvide(sock
, formats
, inData
) {
2654 // Deflate incomming data and their sizes
2655 let deflator
= new Deflator();
2656 let dataToDeflate
= [];
2658 for (let i
= 0; i
< formats
.length
; i
++) {
2659 // We only support the format Text at this time
2660 if (formats
[i
] != extendedClipboardFormatText
) {
2661 throw new Error("Unsupported extended clipboard format for Provide message.");
2664 // Change lone \r or \n into \r\n as defined in rfbproto
2665 inData
[i
] = inData
[i
].replace(/\r\n|\r|\n/gm, "\r\n");
2667 // Check if it already has \0
2668 let text
= encodeUTF8(inData
[i
] + "\0");
2670 dataToDeflate
.push( (text
.length
>> 24) & 0xFF,
2671 (text
.length
>> 16) & 0xFF,
2672 (text
.length
>> 8) & 0xFF,
2673 (text
.length
& 0xFF));
2675 for (let j
= 0; j
< text
.length
; j
++) {
2676 dataToDeflate
.push(text
.charCodeAt(j
));
2680 let deflatedData
= deflator
.deflate(new Uint8Array(dataToDeflate
));
2682 // Build data to send
2683 let data
= new Uint8Array(4 + deflatedData
.length
);
2684 data
.set(RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionProvide
],
2686 data
.set(deflatedData
, 4);
2688 RFB
.messages
.clientCutText(sock
, data
, true);
2691 extendedClipboardNotify(sock
, formats
) {
2692 let flags
= RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionNotify
],
2694 RFB
.messages
.clientCutText(sock
, flags
, true);
2697 extendedClipboardRequest(sock
, formats
) {
2698 let flags
= RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionRequest
],
2700 RFB
.messages
.clientCutText(sock
, flags
, true);
2703 extendedClipboardCaps(sock
, actions
, formats
) {
2704 let formatKeys
= Object
.keys(formats
);
2705 let data
= new Uint8Array(4 + (4 * formatKeys
.length
));
2707 formatKeys
.map(x
=> parseInt(x
));
2708 formatKeys
.sort((a
, b
) => a
- b
);
2710 data
.set(RFB
.messages
._buildExtendedClipboardFlags(actions
, []));
2713 for (let i
= 0; i
< formatKeys
.length
; i
++) {
2714 data
[loopOffset
] = formats
[formatKeys
[i
]] >> 24;
2715 data
[loopOffset
+ 1] = formats
[formatKeys
[i
]] >> 16;
2716 data
[loopOffset
+ 2] = formats
[formatKeys
[i
]] >> 8;
2717 data
[loopOffset
+ 3] = formats
[formatKeys
[i
]] >> 0;
2720 data
[3] |= (1 << formatKeys
[i
]); // Update our format flags
2723 RFB
.messages
.clientCutText(sock
, data
, true);
2726 clientCutText(sock
, data
, extended
= false) {
2727 const buff
= sock
._sQ
;
2728 const offset
= sock
._sQlen
;
2730 buff
[offset
] = 6; // msg-type
2732 buff
[offset
+ 1] = 0; // padding
2733 buff
[offset
+ 2] = 0; // padding
2734 buff
[offset
+ 3] = 0; // padding
2738 length
= toUnsigned32bit(-data
.length
);
2740 length
= data
.length
;
2743 buff
[offset
+ 4] = length
>> 24;
2744 buff
[offset
+ 5] = length
>> 16;
2745 buff
[offset
+ 6] = length
>> 8;
2746 buff
[offset
+ 7] = length
;
2750 // We have to keep track of from where in the data we begin creating the
2751 // buffer for the flush in the next iteration.
2754 let remaining
= data
.length
;
2755 while (remaining
> 0) {
2757 let flushSize
= Math
.min(remaining
, (sock
._sQbufferSize
- sock
._sQlen
));
2758 for (let i
= 0; i
< flushSize
; i
++) {
2759 buff
[sock
._sQlen
+ i
] = data
[dataOffset
+ i
];
2762 sock
._sQlen
+= flushSize
;
2765 remaining
-= flushSize
;
2766 dataOffset
+= flushSize
;
2771 setDesktopSize(sock
, width
, height
, id
, flags
) {
2772 const buff
= sock
._sQ
;
2773 const offset
= sock
._sQlen
;
2775 buff
[offset
] = 251; // msg-type
2776 buff
[offset
+ 1] = 0; // padding
2777 buff
[offset
+ 2] = width
>> 8; // width
2778 buff
[offset
+ 3] = width
;
2779 buff
[offset
+ 4] = height
>> 8; // height
2780 buff
[offset
+ 5] = height
;
2782 buff
[offset
+ 6] = 1; // number-of-screens
2783 buff
[offset
+ 7] = 0; // padding
2786 buff
[offset
+ 8] = id
>> 24; // id
2787 buff
[offset
+ 9] = id
>> 16;
2788 buff
[offset
+ 10] = id
>> 8;
2789 buff
[offset
+ 11] = id
;
2790 buff
[offset
+ 12] = 0; // x-position
2791 buff
[offset
+ 13] = 0;
2792 buff
[offset
+ 14] = 0; // y-position
2793 buff
[offset
+ 15] = 0;
2794 buff
[offset
+ 16] = width
>> 8; // width
2795 buff
[offset
+ 17] = width
;
2796 buff
[offset
+ 18] = height
>> 8; // height
2797 buff
[offset
+ 19] = height
;
2798 buff
[offset
+ 20] = flags
>> 24; // flags
2799 buff
[offset
+ 21] = flags
>> 16;
2800 buff
[offset
+ 22] = flags
>> 8;
2801 buff
[offset
+ 23] = flags
;
2807 clientFence(sock
, flags
, payload
) {
2808 const buff
= sock
._sQ
;
2809 const offset
= sock
._sQlen
;
2811 buff
[offset
] = 248; // msg-type
2813 buff
[offset
+ 1] = 0; // padding
2814 buff
[offset
+ 2] = 0; // padding
2815 buff
[offset
+ 3] = 0; // padding
2817 buff
[offset
+ 4] = flags
>> 24; // flags
2818 buff
[offset
+ 5] = flags
>> 16;
2819 buff
[offset
+ 6] = flags
>> 8;
2820 buff
[offset
+ 7] = flags
;
2822 const n
= payload
.length
;
2824 buff
[offset
+ 8] = n
; // length
2826 for (let i
= 0; i
< n
; i
++) {
2827 buff
[offset
+ 9 + i
] = payload
.charCodeAt(i
);
2830 sock
._sQlen
+= 9 + n
;
2834 enableContinuousUpdates(sock
, enable
, x
, y
, width
, height
) {
2835 const buff
= sock
._sQ
;
2836 const offset
= sock
._sQlen
;
2838 buff
[offset
] = 150; // msg-type
2839 buff
[offset
+ 1] = enable
; // enable-flag
2841 buff
[offset
+ 2] = x
>> 8; // x
2842 buff
[offset
+ 3] = x
;
2843 buff
[offset
+ 4] = y
>> 8; // y
2844 buff
[offset
+ 5] = y
;
2845 buff
[offset
+ 6] = width
>> 8; // width
2846 buff
[offset
+ 7] = width
;
2847 buff
[offset
+ 8] = height
>> 8; // height
2848 buff
[offset
+ 9] = height
;
2854 pixelFormat(sock
, depth
, trueColor
) {
2855 const buff
= sock
._sQ
;
2856 const offset
= sock
._sQlen
;
2862 } else if (depth
> 8) {
2868 const bits
= Math
.floor(depth
/3);
2870 buff
[offset
] = 0; // msg-type
2872 buff
[offset
+ 1] = 0; // padding
2873 buff
[offset
+ 2] = 0; // padding
2874 buff
[offset
+ 3] = 0; // padding
2876 buff
[offset
+ 4] = bpp
; // bits-per-pixel
2877 buff
[offset
+ 5] = depth
; // depth
2878 buff
[offset
+ 6] = 0; // little-endian
2879 buff
[offset
+ 7] = trueColor
? 1 : 0; // true-color
2881 buff
[offset
+ 8] = 0; // red-max
2882 buff
[offset
+ 9] = (1 << bits
) - 1; // red-max
2884 buff
[offset
+ 10] = 0; // green-max
2885 buff
[offset
+ 11] = (1 << bits
) - 1; // green-max
2887 buff
[offset
+ 12] = 0; // blue-max
2888 buff
[offset
+ 13] = (1 << bits
) - 1; // blue-max
2890 buff
[offset
+ 14] = bits
* 0; // red-shift
2891 buff
[offset
+ 15] = bits
* 1; // green-shift
2892 buff
[offset
+ 16] = bits
* 2; // blue-shift
2894 buff
[offset
+ 17] = 0; // padding
2895 buff
[offset
+ 18] = 0; // padding
2896 buff
[offset
+ 19] = 0; // padding
2902 clientEncodings(sock
, encodings
) {
2903 const buff
= sock
._sQ
;
2904 const offset
= sock
._sQlen
;
2906 buff
[offset
] = 2; // msg-type
2907 buff
[offset
+ 1] = 0; // padding
2909 buff
[offset
+ 2] = encodings
.length
>> 8;
2910 buff
[offset
+ 3] = encodings
.length
;
2913 for (let i
= 0; i
< encodings
.length
; i
++) {
2914 const enc
= encodings
[i
];
2915 buff
[j
] = enc
>> 24;
2916 buff
[j
+ 1] = enc
>> 16;
2917 buff
[j
+ 2] = enc
>> 8;
2923 sock
._sQlen
+= j
- offset
;
2927 fbUpdateRequest(sock
, incremental
, x
, y
, w
, h
) {
2928 const buff
= sock
._sQ
;
2929 const offset
= sock
._sQlen
;
2931 if (typeof(x
) === "undefined") { x
= 0; }
2932 if (typeof(y
) === "undefined") { y
= 0; }
2934 buff
[offset
] = 3; // msg-type
2935 buff
[offset
+ 1] = incremental
? 1 : 0;
2937 buff
[offset
+ 2] = (x
>> 8) & 0xFF;
2938 buff
[offset
+ 3] = x
& 0xFF;
2940 buff
[offset
+ 4] = (y
>> 8) & 0xFF;
2941 buff
[offset
+ 5] = y
& 0xFF;
2943 buff
[offset
+ 6] = (w
>> 8) & 0xFF;
2944 buff
[offset
+ 7] = w
& 0xFF;
2946 buff
[offset
+ 8] = (h
>> 8) & 0xFF;
2947 buff
[offset
+ 9] = h
& 0xFF;
2953 xvpOp(sock
, ver
, op
) {
2954 const buff
= sock
._sQ
;
2955 const offset
= sock
._sQlen
;
2957 buff
[offset
] = 250; // msg-type
2958 buff
[offset
+ 1] = 0; // padding
2960 buff
[offset
+ 2] = ver
;
2961 buff
[offset
+ 3] = op
;
2970 rgbaPixels
: new Uint8Array(),
2976 /* eslint-disable indent */
2977 rgbaPixels
: new Uint8Array([
2978 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2979 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
2980 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2982 /* eslint-enable indent */