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('message', () => {
238 this._handleMessage();
240 this._sock
.on('open', () => {
241 if ((this._rfbConnectionState
=== 'connecting') &&
242 (this._rfbInitState
=== '')) {
243 this._rfbInitState
= 'ProtocolVersion';
244 Log
.Debug("Starting VNC handshake");
246 this._fail("Unexpected server connection while " +
247 this._rfbConnectionState
);
250 this._sock
.on('close', (e
) => {
251 Log
.Debug("WebSocket on-close event");
254 msg
= "(code: " + e
.code
;
256 msg
+= ", reason: " + e
.reason
;
260 switch (this._rfbConnectionState
) {
262 this._fail("Connection closed " + msg
);
265 // Handle disconnects that were initiated server-side
266 this._updateConnectionState('disconnecting');
267 this._updateConnectionState('disconnected');
269 case 'disconnecting':
270 // Normal disconnection path
271 this._updateConnectionState('disconnected');
274 this._fail("Unexpected server disconnect " +
275 "when already disconnected " + msg
);
278 this._fail("Unexpected server disconnect before connecting " +
282 this._sock
.off('close');
283 // Delete reference to raw channel to allow cleanup.
284 this._rawChannel
= null;
286 this._sock
.on('error', e
=> Log
.Warn("WebSocket on-error event"));
288 // Slight delay of the actual connection so that the caller has
289 // time to set up callbacks
290 setTimeout(this._updateConnectionState
.bind(this, 'connecting'));
292 Log
.Debug("<< RFB.constructor");
294 // ===== PROPERTIES =====
296 this.dragViewport
= false;
297 this.focusOnClick
= true;
299 this._viewOnly
= false;
300 this._clipViewport
= false;
301 this._scaleViewport
= false;
302 this._resizeSession
= false;
304 this._showDotCursor
= false;
305 if (options
.showDotCursor
!== undefined) {
306 Log
.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated");
307 this._showDotCursor
= options
.showDotCursor
;
310 this._qualityLevel
= 6;
311 this._compressionLevel
= 2;
314 // ===== PROPERTIES =====
316 get viewOnly() { return this._viewOnly
; }
317 set viewOnly(viewOnly
) {
318 this._viewOnly
= viewOnly
;
320 if (this._rfbConnectionState
=== "connecting" ||
321 this._rfbConnectionState
=== "connected") {
323 this._keyboard
.ungrab();
325 this._keyboard
.grab();
330 get capabilities() { return this._capabilities
; }
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');
421 sendCredentials(creds
) {
422 this._rfbCredentials
= creds
;
423 setTimeout(this._initMsg
.bind(this), 0);
427 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
428 Log
.Info("Sending Ctrl-Alt-Del");
430 this.sendKey(KeyTable
.XK_Control_L
, "ControlLeft", true);
431 this.sendKey(KeyTable
.XK_Alt_L
, "AltLeft", true);
432 this.sendKey(KeyTable
.XK_Delete
, "Delete", true);
433 this.sendKey(KeyTable
.XK_Delete
, "Delete", false);
434 this.sendKey(KeyTable
.XK_Alt_L
, "AltLeft", false);
435 this.sendKey(KeyTable
.XK_Control_L
, "ControlLeft", false);
450 // Send a key press. If 'down' is not specified then send a down key
451 // followed by an up key.
452 sendKey(keysym
, code
, down
) {
453 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
455 if (down
=== undefined) {
456 this.sendKey(keysym
, code
, true);
457 this.sendKey(keysym
, code
, false);
461 const scancode
= XtScancode
[code
];
463 if (this._qemuExtKeyEventSupported
&& scancode
) {
465 keysym
= keysym
|| 0;
467 Log
.Info("Sending key (" + (down
? "down" : "up") + "): keysym " + keysym
+ ", scancode " + scancode
);
469 RFB
.messages
.QEMUExtendedKeyEvent(this._sock
, keysym
, down
, scancode
);
474 Log
.Info("Sending keysym (" + (down
? "down" : "up") + "): " + keysym
);
475 RFB
.messages
.keyEvent(this._sock
, keysym
, down
? 1 : 0);
480 this._canvas
.focus();
487 clipboardPasteFrom(text
) {
488 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
490 if (this._clipboardServerCapabilitiesFormats
[extendedClipboardFormatText
] &&
491 this._clipboardServerCapabilitiesActions
[extendedClipboardActionNotify
]) {
493 this._clipboardText
= text
;
494 RFB
.messages
.extendedClipboardNotify(this._sock
, [extendedClipboardFormatText
]);
496 let data
= new Uint8Array(text
.length
);
497 for (let i
= 0; i
< text
.length
; i
++) {
498 // FIXME: text can have values outside of Latin1/Uint8
499 data
[i
] = text
.charCodeAt(i
);
502 RFB
.messages
.clientCutText(this._sock
, data
);
506 // ===== PRIVATE METHODS =====
509 Log
.Debug(">> RFB.connect");
513 Log
.Info(`connecting to ${this._url}`);
514 this._sock
.open(this._url
, this._wsProtocols
);
516 if (e
.name
=== 'SyntaxError') {
517 this._fail("Invalid host or port (" + e
+ ")");
519 this._fail("Error when opening socket (" + e
+ ")");
524 Log
.Info(`attaching ${this._rawChannel} to Websock`);
525 this._sock
.attach(this._rawChannel
);
527 this._fail("Error attaching channel (" + e
+ ")");
531 // Make our elements part of the page
532 this._target
.appendChild(this._screen
);
534 this._gestures
.attach(this._canvas
);
536 this._cursor
.attach(this._canvas
);
537 this._refreshCursor();
539 // Monitor size changes of the screen
540 // FIXME: Use ResizeObserver, or hidden overflow
541 window
.addEventListener('resize', this._eventHandlers
.windowResize
);
543 // Always grab focus on some kind of click event
544 this._canvas
.addEventListener("mousedown", this._eventHandlers
.focusCanvas
);
545 this._canvas
.addEventListener("touchstart", this._eventHandlers
.focusCanvas
);
548 this._canvas
.addEventListener('mousedown', this._eventHandlers
.handleMouse
);
549 this._canvas
.addEventListener('mouseup', this._eventHandlers
.handleMouse
);
550 this._canvas
.addEventListener('mousemove', this._eventHandlers
.handleMouse
);
551 // Prevent middle-click pasting (see handler for why we bind to document)
552 this._canvas
.addEventListener('click', this._eventHandlers
.handleMouse
);
553 // preventDefault() on mousedown doesn't stop this event for some
554 // reason so we have to explicitly block it
555 this._canvas
.addEventListener('contextmenu', this._eventHandlers
.handleMouse
);
558 this._canvas
.addEventListener("wheel", this._eventHandlers
.handleWheel
);
561 this._canvas
.addEventListener("gesturestart", this._eventHandlers
.handleGesture
);
562 this._canvas
.addEventListener("gesturemove", this._eventHandlers
.handleGesture
);
563 this._canvas
.addEventListener("gestureend", this._eventHandlers
.handleGesture
);
565 Log
.Debug("<< RFB.connect");
569 Log
.Debug(">> RFB.disconnect");
570 this._cursor
.detach();
571 this._canvas
.removeEventListener("gesturestart", this._eventHandlers
.handleGesture
);
572 this._canvas
.removeEventListener("gesturemove", this._eventHandlers
.handleGesture
);
573 this._canvas
.removeEventListener("gestureend", this._eventHandlers
.handleGesture
);
574 this._canvas
.removeEventListener("wheel", this._eventHandlers
.handleWheel
);
575 this._canvas
.removeEventListener('mousedown', this._eventHandlers
.handleMouse
);
576 this._canvas
.removeEventListener('mouseup', this._eventHandlers
.handleMouse
);
577 this._canvas
.removeEventListener('mousemove', this._eventHandlers
.handleMouse
);
578 this._canvas
.removeEventListener('click', this._eventHandlers
.handleMouse
);
579 this._canvas
.removeEventListener('contextmenu', this._eventHandlers
.handleMouse
);
580 this._canvas
.removeEventListener("mousedown", this._eventHandlers
.focusCanvas
);
581 this._canvas
.removeEventListener("touchstart", this._eventHandlers
.focusCanvas
);
582 window
.removeEventListener('resize', this._eventHandlers
.windowResize
);
583 this._keyboard
.ungrab();
584 this._gestures
.detach();
587 this._target
.removeChild(this._screen
);
589 if (e
.name
=== 'NotFoundError') {
590 // Some cases where the initial connection fails
591 // can disconnect before the _screen is created
596 clearTimeout(this._resizeTimeout
);
597 clearTimeout(this._mouseMoveTimer
);
598 Log
.Debug("<< RFB.disconnect");
601 _focusCanvas(event
) {
602 if (!this.focusOnClick
) {
609 _setDesktopName(name
) {
611 this.dispatchEvent(new CustomEvent(
613 { detail
: { name
: this._fbName
} }));
616 _windowResize(event
) {
617 // If the window resized then our screen element might have
618 // as well. Update the viewport dimensions.
619 window
.requestAnimationFrame(() => {
624 if (this._resizeSession
) {
625 // Request changing the resolution of the remote display to
626 // the size of the local browser viewport.
628 // In order to not send multiple requests before the browser-resize
629 // is finished we wait 0.5 seconds before sending the request.
630 clearTimeout(this._resizeTimeout
);
631 this._resizeTimeout
= setTimeout(this._requestRemoteResize
.bind(this), 500);
635 // Update state of clipping in Display object, and make sure the
636 // configured viewport matches the current screen size
638 const curClip
= this._display
.clipViewport
;
639 let newClip
= this._clipViewport
;
641 if (this._scaleViewport
) {
642 // Disable viewport clipping if we are scaling
646 if (curClip
!== newClip
) {
647 this._display
.clipViewport
= newClip
;
651 // When clipping is enabled, the screen is limited to
652 // the size of the container.
653 const size
= this._screenSize();
654 this._display
.viewportChangeSize(size
.w
, size
.h
);
655 this._fixScrollbars();
660 if (!this._scaleViewport
) {
661 this._display
.scale
= 1.0;
663 const size
= this._screenSize();
664 this._display
.autoscale(size
.w
, size
.h
);
666 this._fixScrollbars();
669 // Requests a change of remote desktop size. This message is an extension
670 // and may only be sent if we have received an ExtendedDesktopSize message
671 _requestRemoteResize() {
672 clearTimeout(this._resizeTimeout
);
673 this._resizeTimeout
= null;
675 if (!this._resizeSession
|| this._viewOnly
||
676 !this._supportsSetDesktopSize
) {
680 const size
= this._screenSize();
681 RFB
.messages
.setDesktopSize(this._sock
,
682 Math
.floor(size
.w
), Math
.floor(size
.h
),
683 this._screenID
, this._screenFlags
);
685 Log
.Debug('Requested new desktop size: ' +
686 size
.w
+ 'x' + size
.h
);
689 // Gets the the size of the available screen
691 let r
= this._screen
.getBoundingClientRect();
692 return { w
: r
.width
, h
: r
.height
};
696 // This is a hack because Chrome screws up the calculation
697 // for when scrollbars are needed. So to fix it we temporarily
698 // toggle them off and on.
699 const orig
= this._screen
.style
.overflow
;
700 this._screen
.style
.overflow
= 'hidden';
701 // Force Chrome to recalculate the layout by asking for
702 // an element's dimensions
703 this._screen
.getBoundingClientRect();
704 this._screen
.style
.overflow
= orig
;
712 * disconnected - permanent state
714 _updateConnectionState(state
) {
715 const oldstate
= this._rfbConnectionState
;
717 if (state
=== oldstate
) {
718 Log
.Debug("Already in state '" + state
+ "', ignoring");
722 // The 'disconnected' state is permanent for each RFB object
723 if (oldstate
=== 'disconnected') {
724 Log
.Error("Tried changing state of a disconnected RFB object");
728 // Ensure proper transitions before doing anything
731 if (oldstate
!== 'connecting') {
732 Log
.Error("Bad transition to connected state, " +
733 "previous connection state: " + oldstate
);
739 if (oldstate
!== 'disconnecting') {
740 Log
.Error("Bad transition to disconnected state, " +
741 "previous connection state: " + oldstate
);
747 if (oldstate
!== '') {
748 Log
.Error("Bad transition to connecting state, " +
749 "previous connection state: " + oldstate
);
754 case 'disconnecting':
755 if (oldstate
!== 'connected' && oldstate
!== 'connecting') {
756 Log
.Error("Bad transition to disconnecting state, " +
757 "previous connection state: " + oldstate
);
763 Log
.Error("Unknown connection state: " + state
);
767 // State change actions
769 this._rfbConnectionState
= state
;
771 Log
.Debug("New state '" + state
+ "', was '" + oldstate
+ "'.");
773 if (this._disconnTimer
&& state
!== 'disconnecting') {
774 Log
.Debug("Clearing disconnect timer");
775 clearTimeout(this._disconnTimer
);
776 this._disconnTimer
= null;
778 // make sure we don't get a double event
779 this._sock
.off('close');
788 this.dispatchEvent(new CustomEvent("connect", { detail
: {} }));
791 case 'disconnecting':
794 this._disconnTimer
= setTimeout(() => {
795 Log
.Error("Disconnection timed out.");
796 this._updateConnectionState('disconnected');
797 }, DISCONNECT_TIMEOUT
* 1000);
801 this.dispatchEvent(new CustomEvent(
802 "disconnect", { detail
:
803 { clean
: this._rfbCleanDisconnect
} }));
808 /* Print errors and disconnect
810 * The parameter 'details' is used for information that
811 * should be logged but not sent to the user interface.
814 switch (this._rfbConnectionState
) {
815 case 'disconnecting':
816 Log
.Error("Failed when disconnecting: " + details
);
819 Log
.Error("Failed while connected: " + details
);
822 Log
.Error("Failed when connecting: " + details
);
825 Log
.Error("RFB failure: " + details
);
828 this._rfbCleanDisconnect
= false; //This is sent to the UI
830 // Transition to disconnected without waiting for socket to close
831 this._updateConnectionState('disconnecting');
832 this._updateConnectionState('disconnected');
837 _setCapability(cap
, val
) {
838 this._capabilities
[cap
] = val
;
839 this.dispatchEvent(new CustomEvent("capabilities",
840 { detail
: { capabilities
: this._capabilities
} }));
844 if (this._sock
.rQlen
=== 0) {
845 Log
.Warn("handleMessage called on an empty receive queue");
849 switch (this._rfbConnectionState
) {
851 Log
.Error("Got data while disconnected");
855 if (this._flushing
) {
858 if (!this._normalMsg()) {
861 if (this._sock
.rQlen
=== 0) {
872 _handleKeyEvent(keysym
, code
, down
) {
873 this.sendKey(keysym
, code
, down
);
878 * We don't check connection status or viewOnly here as the
879 * mouse events might be used to control the viewport
882 if (ev
.type
=== 'click') {
884 * Note: This is only needed for the 'click' event as it fails
885 * to fire properly for the target element so we have
886 * to listen on the document element instead.
888 if (ev
.target
!== this._canvas
) {
893 // FIXME: if we're in view-only and not dragging,
894 // should we stop events?
895 ev
.stopPropagation();
898 if ((ev
.type
=== 'click') || (ev
.type
=== 'contextmenu')) {
902 let pos
= clientToElement(ev
.clientX
, ev
.clientY
,
907 setCapture(this._canvas
);
908 this._handleMouseButton(pos
.x
, pos
.y
,
909 true, 1 << ev
.button
);
912 this._handleMouseButton(pos
.x
, pos
.y
,
913 false, 1 << ev
.button
);
916 this._handleMouseMove(pos
.x
, pos
.y
);
921 _handleMouseButton(x
, y
, down
, bmask
) {
922 if (this.dragViewport
) {
923 if (down
&& !this._viewportDragging
) {
924 this._viewportDragging
= true;
925 this._viewportDragPos
= {'x': x
, 'y': y
};
926 this._viewportHasMoved
= false;
928 // Skip sending mouse events
931 this._viewportDragging
= false;
933 // If we actually performed a drag then we are done
934 // here and should not send any mouse events
935 if (this._viewportHasMoved
) {
939 // Otherwise we treat this as a mouse click event.
940 // Send the button down event here, as the button up
941 // event is sent at the end of this function.
942 this._sendMouse(x
, y
, bmask
);
946 // Flush waiting move event first
947 if (this._mouseMoveTimer
!== null) {
948 clearTimeout(this._mouseMoveTimer
);
949 this._mouseMoveTimer
= null;
950 this._sendMouse(x
, y
, this._mouseButtonMask
);
954 this._mouseButtonMask
|= bmask
;
956 this._mouseButtonMask
&= ~bmask
;
959 this._sendMouse(x
, y
, this._mouseButtonMask
);
962 _handleMouseMove(x
, y
) {
963 if (this._viewportDragging
) {
964 const deltaX
= this._viewportDragPos
.x
- x
;
965 const deltaY
= this._viewportDragPos
.y
- y
;
967 if (this._viewportHasMoved
|| (Math
.abs(deltaX
) > dragThreshold
||
968 Math
.abs(deltaY
) > dragThreshold
)) {
969 this._viewportHasMoved
= true;
971 this._viewportDragPos
= {'x': x
, 'y': y
};
972 this._display
.viewportChangePos(deltaX
, deltaY
);
975 // Skip sending mouse events
979 this._mousePos
= { 'x': x
, 'y': y
};
981 // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms
982 if (this._mouseMoveTimer
== null) {
984 const timeSinceLastMove
= Date
.now() - this._mouseLastMoveTime
;
985 if (timeSinceLastMove
> MOUSE_MOVE_DELAY
) {
986 this._sendMouse(x
, y
, this._mouseButtonMask
);
987 this._mouseLastMoveTime
= Date
.now();
989 // Too soon since the latest move, wait the remaining time
990 this._mouseMoveTimer
= setTimeout(() => {
991 this._handleDelayedMouseMove();
992 }, MOUSE_MOVE_DELAY
- timeSinceLastMove
);
997 _handleDelayedMouseMove() {
998 this._mouseMoveTimer
= null;
999 this._sendMouse(this._mousePos
.x
, this._mousePos
.y
,
1000 this._mouseButtonMask
);
1001 this._mouseLastMoveTime
= Date
.now();
1004 _sendMouse(x
, y
, mask
) {
1005 if (this._rfbConnectionState
!== 'connected') { return; }
1006 if (this._viewOnly
) { return; } // View only, skip mouse events
1008 RFB
.messages
.pointerEvent(this._sock
, this._display
.absX(x
),
1009 this._display
.absY(y
), mask
);
1013 if (this._rfbConnectionState
!== 'connected') { return; }
1014 if (this._viewOnly
) { return; } // View only, skip mouse events
1016 ev
.stopPropagation();
1017 ev
.preventDefault();
1019 let pos
= clientToElement(ev
.clientX
, ev
.clientY
,
1025 // Pixel units unless it's non-zero.
1026 // Note that if deltamode is line or page won't matter since we aren't
1027 // sending the mouse wheel delta to the server anyway.
1028 // The difference between pixel and line can be important however since
1029 // we have a threshold that can be smaller than the line height.
1030 if (ev
.deltaMode
!== 0) {
1031 dX
*= WHEEL_LINE_HEIGHT
;
1032 dY
*= WHEEL_LINE_HEIGHT
;
1035 // Mouse wheel events are sent in steps over VNC. This means that the VNC
1036 // protocol can't handle a wheel event with specific distance or speed.
1037 // Therefor, if we get a lot of small mouse wheel events we combine them.
1038 this._accumulatedWheelDeltaX
+= dX
;
1039 this._accumulatedWheelDeltaY
+= dY
;
1041 // Generate a mouse wheel step event when the accumulated delta
1042 // for one of the axes is large enough.
1043 if (Math
.abs(this._accumulatedWheelDeltaX
) >= WHEEL_STEP
) {
1044 if (this._accumulatedWheelDeltaX
< 0) {
1045 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 5);
1046 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 5);
1047 } else if (this._accumulatedWheelDeltaX
> 0) {
1048 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 6);
1049 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 6);
1052 this._accumulatedWheelDeltaX
= 0;
1054 if (Math
.abs(this._accumulatedWheelDeltaY
) >= WHEEL_STEP
) {
1055 if (this._accumulatedWheelDeltaY
< 0) {
1056 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 3);
1057 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 3);
1058 } else if (this._accumulatedWheelDeltaY
> 0) {
1059 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 4);
1060 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 4);
1063 this._accumulatedWheelDeltaY
= 0;
1067 _fakeMouseMove(ev
, elementX
, elementY
) {
1068 this._handleMouseMove(elementX
, elementY
);
1069 this._cursor
.move(ev
.detail
.clientX
, ev
.detail
.clientY
);
1072 _handleTapEvent(ev
, bmask
) {
1073 let pos
= clientToElement(ev
.detail
.clientX
, ev
.detail
.clientY
,
1076 // If the user quickly taps multiple times we assume they meant to
1077 // hit the same spot, so slightly adjust coordinates
1079 if ((this._gestureLastTapTime
!== null) &&
1080 ((Date
.now() - this._gestureLastTapTime
) < DOUBLE_TAP_TIMEOUT
) &&
1081 (this._gestureFirstDoubleTapEv
.detail
.type
=== ev
.detail
.type
)) {
1082 let dx
= this._gestureFirstDoubleTapEv
.detail
.clientX
- ev
.detail
.clientX
;
1083 let dy
= this._gestureFirstDoubleTapEv
.detail
.clientY
- ev
.detail
.clientY
;
1084 let distance
= Math
.hypot(dx
, dy
);
1086 if (distance
< DOUBLE_TAP_THRESHOLD
) {
1087 pos
= clientToElement(this._gestureFirstDoubleTapEv
.detail
.clientX
,
1088 this._gestureFirstDoubleTapEv
.detail
.clientY
,
1091 this._gestureFirstDoubleTapEv
= ev
;
1094 this._gestureFirstDoubleTapEv
= ev
;
1096 this._gestureLastTapTime
= Date
.now();
1098 this._fakeMouseMove(this._gestureFirstDoubleTapEv
, pos
.x
, pos
.y
);
1099 this._handleMouseButton(pos
.x
, pos
.y
, true, bmask
);
1100 this._handleMouseButton(pos
.x
, pos
.y
, false, bmask
);
1103 _handleGesture(ev
) {
1106 let pos
= clientToElement(ev
.detail
.clientX
, ev
.detail
.clientY
,
1109 case 'gesturestart':
1110 switch (ev
.detail
.type
) {
1112 this._handleTapEvent(ev
, 0x1);
1115 this._handleTapEvent(ev
, 0x4);
1118 this._handleTapEvent(ev
, 0x2);
1121 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1122 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x1);
1125 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1126 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x4);
1130 this._gestureLastMagnitudeX
= ev
.detail
.magnitudeX
;
1131 this._gestureLastMagnitudeY
= ev
.detail
.magnitudeY
;
1132 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1135 this._gestureLastMagnitudeX
= Math
.hypot(ev
.detail
.magnitudeX
,
1136 ev
.detail
.magnitudeY
);
1137 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1143 switch (ev
.detail
.type
) {
1150 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1153 // Always scroll in the same position.
1154 // We don't know if the mouse was moved so we need to move it
1156 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1157 while ((ev
.detail
.magnitudeY
- this._gestureLastMagnitudeY
) > GESTURE_SCRLSENS
) {
1158 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x8);
1159 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x8);
1160 this._gestureLastMagnitudeY
+= GESTURE_SCRLSENS
;
1162 while ((ev
.detail
.magnitudeY
- this._gestureLastMagnitudeY
) < -GESTURE_SCRLSENS
) {
1163 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x10);
1164 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x10);
1165 this._gestureLastMagnitudeY
-= GESTURE_SCRLSENS
;
1167 while ((ev
.detail
.magnitudeX
- this._gestureLastMagnitudeX
) > GESTURE_SCRLSENS
) {
1168 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x20);
1169 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x20);
1170 this._gestureLastMagnitudeX
+= GESTURE_SCRLSENS
;
1172 while ((ev
.detail
.magnitudeX
- this._gestureLastMagnitudeX
) < -GESTURE_SCRLSENS
) {
1173 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x40);
1174 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x40);
1175 this._gestureLastMagnitudeX
-= GESTURE_SCRLSENS
;
1179 // Always scroll in the same position.
1180 // We don't know if the mouse was moved so we need to move it
1182 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1183 magnitude
= Math
.hypot(ev
.detail
.magnitudeX
, ev
.detail
.magnitudeY
);
1184 if (Math
.abs(magnitude
- this._gestureLastMagnitudeX
) > GESTURE_ZOOMSENS
) {
1185 this._handleKeyEvent(KeyTable
.XK_Control_L
, "ControlLeft", true);
1186 while ((magnitude
- this._gestureLastMagnitudeX
) > GESTURE_ZOOMSENS
) {
1187 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x8);
1188 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x8);
1189 this._gestureLastMagnitudeX
+= GESTURE_ZOOMSENS
;
1191 while ((magnitude
- this._gestureLastMagnitudeX
) < -GESTURE_ZOOMSENS
) {
1192 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x10);
1193 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x10);
1194 this._gestureLastMagnitudeX
-= GESTURE_ZOOMSENS
;
1197 this._handleKeyEvent(KeyTable
.XK_Control_L
, "ControlLeft", false);
1203 switch (ev
.detail
.type
) {
1211 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1212 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x1);
1215 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1216 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x4);
1225 _negotiateProtocolVersion() {
1226 if (this._sock
.rQwait("version", 12)) {
1230 const sversion
= this._sock
.rQshiftStr(12).substr(4, 7);
1231 Log
.Info("Server ProtocolVersion: " + sversion
);
1234 case "000.000": // UltraVNC repeater
1238 case "003.006": // UltraVNC
1239 case "003.889": // Apple Remote Desktop
1240 this._rfbVersion
= 3.3;
1243 this._rfbVersion
= 3.7;
1246 case "004.000": // Intel AMT KVM
1247 case "004.001": // RealVNC 4.6
1248 case "005.000": // RealVNC 5.3
1249 this._rfbVersion
= 3.8;
1252 return this._fail("Invalid server version " + sversion
);
1256 let repeaterID
= "ID:" + this._repeaterID
;
1257 while (repeaterID
.length
< 250) {
1260 this._sock
.sendString(repeaterID
);
1264 if (this._rfbVersion
> this._rfbMaxVersion
) {
1265 this._rfbVersion
= this._rfbMaxVersion
;
1268 const cversion
= "00" + parseInt(this._rfbVersion
, 10) +
1269 ".00" + ((this._rfbVersion
* 10) % 10);
1270 this._sock
.sendString("RFB " + cversion
+ "\n");
1271 Log
.Debug('Sent ProtocolVersion: ' + cversion
);
1273 this._rfbInitState
= 'Security';
1276 _negotiateSecurity() {
1277 if (this._rfbVersion
>= 3.7) {
1278 // Server sends supported list, client decides
1279 const numTypes
= this._sock
.rQshift8();
1280 if (this._sock
.rQwait("security type", numTypes
, 1)) { return false; }
1282 if (numTypes
=== 0) {
1283 this._rfbInitState
= "SecurityReason";
1284 this._securityContext
= "no security types";
1285 this._securityStatus
= 1;
1286 return this._initMsg();
1289 const types
= this._sock
.rQshiftBytes(numTypes
);
1290 Log
.Debug("Server security types: " + types
);
1292 // Look for each auth in preferred order
1293 if (types
.includes(1)) {
1294 this._rfbAuthScheme
= 1; // None
1295 } else if (types
.includes(22)) {
1296 this._rfbAuthScheme
= 22; // XVP
1297 } else if (types
.includes(16)) {
1298 this._rfbAuthScheme
= 16; // Tight
1299 } else if (types
.includes(2)) {
1300 this._rfbAuthScheme
= 2; // VNC Auth
1301 } else if (types
.includes(19)) {
1302 this._rfbAuthScheme
= 19; // VeNCrypt Auth
1304 return this._fail("Unsupported security types (types: " + types
+ ")");
1307 this._sock
.send([this._rfbAuthScheme
]);
1310 if (this._sock
.rQwait("security scheme", 4)) { return false; }
1311 this._rfbAuthScheme
= this._sock
.rQshift32();
1313 if (this._rfbAuthScheme
== 0) {
1314 this._rfbInitState
= "SecurityReason";
1315 this._securityContext
= "authentication scheme";
1316 this._securityStatus
= 1;
1317 return this._initMsg();
1321 this._rfbInitState
= 'Authentication';
1322 Log
.Debug('Authenticating using scheme: ' + this._rfbAuthScheme
);
1324 return this._initMsg(); // jump to authentication
1327 _handleSecurityReason() {
1328 if (this._sock
.rQwait("reason length", 4)) {
1331 const strlen
= this._sock
.rQshift32();
1335 if (this._sock
.rQwait("reason", strlen
, 4)) { return false; }
1336 reason
= this._sock
.rQshiftStr(strlen
);
1339 if (reason
!== "") {
1340 this.dispatchEvent(new CustomEvent(
1342 { detail
: { status
: this._securityStatus
,
1343 reason
: reason
} }));
1345 return this._fail("Security negotiation failed on " +
1346 this._securityContext
+
1347 " (reason: " + reason
+ ")");
1349 this.dispatchEvent(new CustomEvent(
1351 { detail
: { status
: this._securityStatus
} }));
1353 return this._fail("Security negotiation failed on " +
1354 this._securityContext
);
1359 _negotiateXvpAuth() {
1360 if (this._rfbCredentials
.username
=== undefined ||
1361 this._rfbCredentials
.password
=== undefined ||
1362 this._rfbCredentials
.target
=== undefined) {
1363 this.dispatchEvent(new CustomEvent(
1364 "credentialsrequired",
1365 { detail
: { types
: ["username", "password", "target"] } }));
1369 const xvpAuthStr
= String
.fromCharCode(this._rfbCredentials
.username
.length
) +
1370 String
.fromCharCode(this._rfbCredentials
.target
.length
) +
1371 this._rfbCredentials
.username
+
1372 this._rfbCredentials
.target
;
1373 this._sock
.sendString(xvpAuthStr
);
1374 this._rfbAuthScheme
= 2;
1375 return this._negotiateAuthentication();
1378 // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
1379 _negotiateVeNCryptAuth() {
1381 // waiting for VeNCrypt version
1382 if (this._rfbVeNCryptState
== 0) {
1383 if (this._sock
.rQwait("vencrypt version", 2)) { return false; }
1385 const major
= this._sock
.rQshift8();
1386 const minor
= this._sock
.rQshift8();
1388 if (!(major
== 0 && minor
== 2)) {
1389 return this._fail("Unsupported VeNCrypt version " + major
+ "." + minor
);
1392 this._sock
.send([0, 2]);
1393 this._rfbVeNCryptState
= 1;
1397 if (this._rfbVeNCryptState
== 1) {
1398 if (this._sock
.rQwait("vencrypt ack", 1)) { return false; }
1400 const res
= this._sock
.rQshift8();
1403 return this._fail("VeNCrypt failure " + res
);
1406 this._rfbVeNCryptState
= 2;
1408 // must fall through here (i.e. no "else if"), beacause we may have already received
1409 // the subtypes length and won't be called again
1411 if (this._rfbVeNCryptState
== 2) { // waiting for subtypes length
1412 if (this._sock
.rQwait("vencrypt subtypes length", 1)) { return false; }
1414 const subtypesLength
= this._sock
.rQshift8();
1415 if (subtypesLength
< 1) {
1416 return this._fail("VeNCrypt subtypes empty");
1419 this._rfbVeNCryptSubtypesLength
= subtypesLength
;
1420 this._rfbVeNCryptState
= 3;
1423 // waiting for subtypes list
1424 if (this._rfbVeNCryptState
== 3) {
1425 if (this._sock
.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength
)) { return false; }
1427 const subtypes
= [];
1428 for (let i
= 0; i
< this._rfbVeNCryptSubtypesLength
; i
++) {
1429 subtypes
.push(this._sock
.rQshift32());
1432 // 256 = Plain subtype
1433 if (subtypes
.indexOf(256) != -1) {
1435 this._sock
.send([0, 0, 1, 0]);
1436 this._rfbVeNCryptState
= 4;
1438 return this._fail("VeNCrypt Plain subtype not offered by server");
1442 // negotiated Plain subtype, server waits for password
1443 if (this._rfbVeNCryptState
== 4) {
1444 if (!this._rfbCredentials
.username
||
1445 !this._rfbCredentials
.password
) {
1446 this.dispatchEvent(new CustomEvent(
1447 "credentialsrequired",
1448 { detail
: { types
: ["username", "password"] } }));
1452 const user
= encodeUTF8(this._rfbCredentials
.username
);
1453 const pass
= encodeUTF8(this._rfbCredentials
.password
);
1456 (user
.length
>> 24) & 0xFF,
1457 (user
.length
>> 16) & 0xFF,
1458 (user
.legnth
>> 8) & 0xFF,
1462 (pass
.length
>> 24) & 0xFF,
1463 (pass
.length
>> 16) & 0xFF,
1464 (pass
.legnth
>> 8) & 0xFF,
1467 this._sock
.sendString(user
);
1468 this._sock
.sendString(pass
);
1470 this._rfbInitState
= "SecurityResult";
1475 _negotiateStdVNCAuth() {
1476 if (this._sock
.rQwait("auth challenge", 16)) { return false; }
1478 if (this._rfbCredentials
.password
=== undefined) {
1479 this.dispatchEvent(new CustomEvent(
1480 "credentialsrequired",
1481 { detail
: { types
: ["password"] } }));
1485 // TODO(directxman12): make genDES not require an Array
1486 const challenge
= Array
.prototype.slice
.call(this._sock
.rQshiftBytes(16));
1487 const response
= RFB
.genDES(this._rfbCredentials
.password
, challenge
);
1488 this._sock
.send(response
);
1489 this._rfbInitState
= "SecurityResult";
1493 _negotiateTightUnixAuth() {
1494 if (this._rfbCredentials
.username
=== undefined ||
1495 this._rfbCredentials
.password
=== undefined) {
1496 this.dispatchEvent(new CustomEvent(
1497 "credentialsrequired",
1498 { detail
: { types
: ["username", "password"] } }));
1502 this._sock
.send([0, 0, 0, this._rfbCredentials
.username
.length
]);
1503 this._sock
.send([0, 0, 0, this._rfbCredentials
.password
.length
]);
1504 this._sock
.sendString(this._rfbCredentials
.username
);
1505 this._sock
.sendString(this._rfbCredentials
.password
);
1506 this._rfbInitState
= "SecurityResult";
1510 _negotiateTightTunnels(numTunnels
) {
1511 const clientSupportedTunnelTypes
= {
1512 0: { vendor
: 'TGHT', signature
: 'NOTUNNEL' }
1514 const serverSupportedTunnelTypes
= {};
1515 // receive tunnel capabilities
1516 for (let i
= 0; i
< numTunnels
; i
++) {
1517 const capCode
= this._sock
.rQshift32();
1518 const capVendor
= this._sock
.rQshiftStr(4);
1519 const capSignature
= this._sock
.rQshiftStr(8);
1520 serverSupportedTunnelTypes
[capCode
] = { vendor
: capVendor
, signature
: capSignature
};
1523 Log
.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes
);
1525 // Siemens touch panels have a VNC server that supports NOTUNNEL,
1526 // but forgets to advertise it. Try to detect such servers by
1527 // looking for their custom tunnel type.
1528 if (serverSupportedTunnelTypes
[1] &&
1529 (serverSupportedTunnelTypes
[1].vendor
=== "SICR") &&
1530 (serverSupportedTunnelTypes
[1].signature
=== "SCHANNEL")) {
1531 Log
.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
1532 serverSupportedTunnelTypes
[0] = { vendor
: 'TGHT', signature
: 'NOTUNNEL' };
1535 // choose the notunnel type
1536 if (serverSupportedTunnelTypes
[0]) {
1537 if (serverSupportedTunnelTypes
[0].vendor
!= clientSupportedTunnelTypes
[0].vendor
||
1538 serverSupportedTunnelTypes
[0].signature
!= clientSupportedTunnelTypes
[0].signature
) {
1539 return this._fail("Client's tunnel type had the incorrect " +
1540 "vendor or signature");
1542 Log
.Debug("Selected tunnel type: " + clientSupportedTunnelTypes
[0]);
1543 this._sock
.send([0, 0, 0, 0]); // use NOTUNNEL
1544 return false; // wait until we receive the sub auth count to continue
1546 return this._fail("Server wanted tunnels, but doesn't support " +
1547 "the notunnel type");
1551 _negotiateTightAuth() {
1552 if (!this._rfbTightVNC
) { // first pass, do the tunnel negotiation
1553 if (this._sock
.rQwait("num tunnels", 4)) { return false; }
1554 const numTunnels
= this._sock
.rQshift32();
1555 if (numTunnels
> 0 && this._sock
.rQwait("tunnel capabilities", 16 * numTunnels
, 4)) { return false; }
1557 this._rfbTightVNC
= true;
1559 if (numTunnels
> 0) {
1560 this._negotiateTightTunnels(numTunnels
);
1561 return false; // wait until we receive the sub auth to continue
1565 // second pass, do the sub-auth negotiation
1566 if (this._sock
.rQwait("sub auth count", 4)) { return false; }
1567 const subAuthCount
= this._sock
.rQshift32();
1568 if (subAuthCount
=== 0) { // empty sub-auth list received means 'no auth' subtype selected
1569 this._rfbInitState
= 'SecurityResult';
1573 if (this._sock
.rQwait("sub auth capabilities", 16 * subAuthCount
, 4)) { return false; }
1575 const clientSupportedTypes
= {
1581 const serverSupportedTypes
= [];
1583 for (let i
= 0; i
< subAuthCount
; i
++) {
1584 this._sock
.rQshift32(); // capNum
1585 const capabilities
= this._sock
.rQshiftStr(12);
1586 serverSupportedTypes
.push(capabilities
);
1589 Log
.Debug("Server Tight authentication types: " + serverSupportedTypes
);
1591 for (let authType
in clientSupportedTypes
) {
1592 if (serverSupportedTypes
.indexOf(authType
) != -1) {
1593 this._sock
.send([0, 0, 0, clientSupportedTypes
[authType
]]);
1594 Log
.Debug("Selected authentication type: " + authType
);
1597 case 'STDVNOAUTH__': // no auth
1598 this._rfbInitState
= 'SecurityResult';
1600 case 'STDVVNCAUTH_': // VNC auth
1601 this._rfbAuthScheme
= 2;
1602 return this._initMsg();
1603 case 'TGHTULGNAUTH': // UNIX auth
1604 this._rfbAuthScheme
= 129;
1605 return this._initMsg();
1607 return this._fail("Unsupported tiny auth scheme " +
1608 "(scheme: " + authType
+ ")");
1613 return this._fail("No supported sub-auth types!");
1616 _negotiateAuthentication() {
1617 switch (this._rfbAuthScheme
) {
1619 if (this._rfbVersion
>= 3.8) {
1620 this._rfbInitState
= 'SecurityResult';
1623 this._rfbInitState
= 'ClientInitialisation';
1624 return this._initMsg();
1626 case 22: // XVP auth
1627 return this._negotiateXvpAuth();
1629 case 2: // VNC authentication
1630 return this._negotiateStdVNCAuth();
1632 case 16: // TightVNC Security Type
1633 return this._negotiateTightAuth();
1635 case 19: // VeNCrypt Security Type
1636 return this._negotiateVeNCryptAuth();
1638 case 129: // TightVNC UNIX Security Type
1639 return this._negotiateTightUnixAuth();
1642 return this._fail("Unsupported auth scheme (scheme: " +
1643 this._rfbAuthScheme
+ ")");
1647 _handleSecurityResult() {
1648 if (this._sock
.rQwait('VNC auth response ', 4)) { return false; }
1650 const status
= this._sock
.rQshift32();
1652 if (status
=== 0) { // OK
1653 this._rfbInitState
= 'ClientInitialisation';
1654 Log
.Debug('Authentication OK');
1655 return this._initMsg();
1657 if (this._rfbVersion
>= 3.8) {
1658 this._rfbInitState
= "SecurityReason";
1659 this._securityContext
= "security result";
1660 this._securityStatus
= status
;
1661 return this._initMsg();
1663 this.dispatchEvent(new CustomEvent(
1665 { detail
: { status
: status
} }));
1667 return this._fail("Security handshake failed");
1672 _negotiateServerInit() {
1673 if (this._sock
.rQwait("server initialization", 24)) { return false; }
1676 const width
= this._sock
.rQshift16();
1677 const height
= this._sock
.rQshift16();
1680 const bpp
= this._sock
.rQshift8();
1681 const depth
= this._sock
.rQshift8();
1682 const bigEndian
= this._sock
.rQshift8();
1683 const trueColor
= this._sock
.rQshift8();
1685 const redMax
= this._sock
.rQshift16();
1686 const greenMax
= this._sock
.rQshift16();
1687 const blueMax
= this._sock
.rQshift16();
1688 const redShift
= this._sock
.rQshift8();
1689 const greenShift
= this._sock
.rQshift8();
1690 const blueShift
= this._sock
.rQshift8();
1691 this._sock
.rQskipBytes(3); // padding
1693 // NB(directxman12): we don't want to call any callbacks or print messages until
1694 // *after* we're past the point where we could backtrack
1696 /* Connection name/title */
1697 const nameLength = this._sock.rQshift32();
1698 if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
1699 let name = this._sock.rQshiftStr(nameLength);
1700 name = decodeUTF8(name, true);
1702 if (this._rfbTightVNC) {
1703 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
1704 // In TightVNC mode, ServerInit message is extended
1705 const numServerMessages = this._sock.rQshift16();
1706 const numClientMessages = this._sock.rQshift16();
1707 const numEncodings = this._sock.rQshift16();
1708 this._sock.rQskipBytes(2); // padding
1710 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
1711 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
1713 // we don't actually do anything with the capability information that TIGHT sends,
1714 // so we just skip the all of this.
1716 // TIGHT server message capabilities
1717 this._sock.rQskipBytes(16 * numServerMessages);
1719 // TIGHT client message capabilities
1720 this._sock.rQskipBytes(16 * numClientMessages);
1722 // TIGHT encoding capabilities
1723 this._sock.rQskipBytes(16 * numEncodings);
1726 // NB(directxman12): these are down here so that we don't run them multiple times
1728 Log.Info("Screen: " + width + "x" + height +
1729 ", bpp: " + bpp + ", depth: " + depth +
1730 ", bigEndian: " + bigEndian +
1731 ", trueColor: " + trueColor +
1732 ", redMax: " + redMax +
1733 ", greenMax: " + greenMax +
1734 ", blueMax: " + blueMax +
1735 ", redShift: " + redShift +
1736 ", greenShift: " + greenShift +
1737 ", blueShift: " + blueShift);
1739 // we're past the point where we could backtrack, so it's safe to call this
1740 this._setDesktopName(name);
1741 this._resize(width, height);
1743 if (!this._viewOnly) { this._keyboard.grab(); }
1747 if (this._fbName === "Intel(r) AMT KVM") {
1748 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
1752 RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
1753 this._sendEncodings();
1754 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
1756 this._updateConnectionState('connected');
1763 // In preference order
1764 encs.push(encodings.encodingCopyRect);
1765 // Only supported with full depth support
1766 if (this._fbDepth == 24) {
1767 encs.push(encodings.encodingTight);
1768 encs.push(encodings.encodingTightPNG);
1769 encs.push(encodings.encodingHextile);
1770 encs.push(encodings.encodingRRE);
1772 encs.push(encodings.encodingRaw);
1774 // Psuedo-encoding settings
1775 encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
1776 encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
1778 encs.push(encodings.pseudoEncodingDesktopSize);
1779 encs.push(encodings.pseudoEncodingLastRect);
1780 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
1781 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
1782 encs.push(encodings.pseudoEncodingXvp);
1783 encs.push(encodings.pseudoEncodingFence);
1784 encs.push(encodings.pseudoEncodingContinuousUpdates);
1785 encs.push(encodings.pseudoEncodingDesktopName);
1786 encs.push(encodings.pseudoEncodingExtendedClipboard);
1788 if (this._fbDepth == 24) {
1789 encs.push(encodings.pseudoEncodingVMwareCursor);
1790 encs.push(encodings.pseudoEncodingCursor);
1793 RFB.messages.clientEncodings(this._sock, encs);
1796 /* RFB protocol initialization states:
1801 * ClientInitialization - not triggered by server message
1802 * ServerInitialization
1805 switch (this._rfbInitState
) {
1806 case 'ProtocolVersion':
1807 return this._negotiateProtocolVersion();
1810 return this._negotiateSecurity();
1812 case 'Authentication':
1813 return this._negotiateAuthentication();
1815 case 'SecurityResult':
1816 return this._handleSecurityResult();
1818 case 'SecurityReason':
1819 return this._handleSecurityReason();
1821 case 'ClientInitialisation':
1822 this._sock
.send([this._shared
? 1 : 0]); // ClientInitialisation
1823 this._rfbInitState
= 'ServerInitialisation';
1826 case 'ServerInitialisation':
1827 return this._negotiateServerInit();
1830 return this._fail("Unknown init state (state: " +
1831 this._rfbInitState
+ ")");
1835 _handleSetColourMapMsg() {
1836 Log
.Debug("SetColorMapEntries");
1838 return this._fail("Unexpected SetColorMapEntries message");
1841 _handleServerCutText() {
1842 Log
.Debug("ServerCutText");
1844 if (this._sock
.rQwait("ServerCutText header", 7, 1)) { return false; }
1846 this._sock
.rQskipBytes(3); // Padding
1848 let length
= this._sock
.rQshift32();
1849 length
= toSigned32bit(length
);
1851 if (this._sock
.rQwait("ServerCutText content", Math
.abs(length
), 8)) { return false; }
1855 const text
= this._sock
.rQshiftStr(length
);
1856 if (this._viewOnly
) {
1860 this.dispatchEvent(new CustomEvent(
1862 { detail
: { text
: text
} }));
1866 length
= Math
.abs(length
);
1867 const flags
= this._sock
.rQshift32();
1868 let formats
= flags
& 0x0000FFFF;
1869 let actions
= flags
& 0xFF000000;
1871 let isCaps
= (!!(actions
& extendedClipboardActionCaps
));
1873 this._clipboardServerCapabilitiesFormats
= {};
1874 this._clipboardServerCapabilitiesActions
= {};
1876 // Update our server capabilities for Formats
1877 for (let i
= 0; i
<= 15; i
++) {
1880 // Check if format flag is set.
1881 if ((formats
& index
)) {
1882 this._clipboardServerCapabilitiesFormats
[index
] = true;
1883 // We don't send unsolicited clipboard, so we
1885 this._sock
.rQshift32();
1889 // Update our server capabilities for Actions
1890 for (let i
= 24; i
<= 31; i
++) {
1892 this._clipboardServerCapabilitiesActions
[index
] = !!(actions
& index
);
1895 /* Caps handling done, send caps with the clients
1896 capabilities set as a response */
1897 let clientActions
= [
1898 extendedClipboardActionCaps
,
1899 extendedClipboardActionRequest
,
1900 extendedClipboardActionPeek
,
1901 extendedClipboardActionNotify
,
1902 extendedClipboardActionProvide
1904 RFB
.messages
.extendedClipboardCaps(this._sock
, clientActions
, {extendedClipboardFormatText
: 0});
1906 } else if (actions
=== extendedClipboardActionRequest
) {
1907 if (this._viewOnly
) {
1911 // Check if server has told us it can handle Provide and there is clipboard data to send.
1912 if (this._clipboardText
!= null &&
1913 this._clipboardServerCapabilitiesActions
[extendedClipboardActionProvide
]) {
1915 if (formats
& extendedClipboardFormatText
) {
1916 RFB
.messages
.extendedClipboardProvide(this._sock
, [extendedClipboardFormatText
], [this._clipboardText
]);
1920 } else if (actions
=== extendedClipboardActionPeek
) {
1921 if (this._viewOnly
) {
1925 if (this._clipboardServerCapabilitiesActions
[extendedClipboardActionNotify
]) {
1927 if (this._clipboardText
!= null) {
1928 RFB
.messages
.extendedClipboardNotify(this._sock
, [extendedClipboardFormatText
]);
1930 RFB
.messages
.extendedClipboardNotify(this._sock
, []);
1934 } else if (actions
=== extendedClipboardActionNotify
) {
1935 if (this._viewOnly
) {
1939 if (this._clipboardServerCapabilitiesActions
[extendedClipboardActionRequest
]) {
1941 if (formats
& extendedClipboardFormatText
) {
1942 RFB
.messages
.extendedClipboardRequest(this._sock
, [extendedClipboardFormatText
]);
1946 } else if (actions
=== extendedClipboardActionProvide
) {
1947 if (this._viewOnly
) {
1951 if (!(formats
& extendedClipboardFormatText
)) {
1954 // Ignore what we had in our clipboard client side.
1955 this._clipboardText
= null;
1957 // FIXME: Should probably verify that this data was actually requested
1958 let zlibStream
= this._sock
.rQshiftBytes(length
- 4);
1959 let streamInflator
= new Inflator();
1960 let textData
= null;
1962 streamInflator
.setInput(zlibStream
);
1963 for (let i
= 0; i
<= 15; i
++) {
1964 let format
= 1 << i
;
1966 if (formats
& format
) {
1969 let sizeArray
= streamInflator
.inflate(4);
1971 size
|= (sizeArray
[0] << 24);
1972 size
|= (sizeArray
[1] << 16);
1973 size
|= (sizeArray
[2] << 8);
1974 size
|= (sizeArray
[3]);
1975 let chunk
= streamInflator
.inflate(size
);
1977 if (format
=== extendedClipboardFormatText
) {
1982 streamInflator
.setInput(null);
1984 if (textData
!== null) {
1986 for (let i
= 0; i
< textData
.length
; i
++) {
1987 tmpText
+= String
.fromCharCode(textData
[i
]);
1991 textData
= decodeUTF8(textData
);
1992 if ((textData
.length
> 0) && "\0" === textData
.charAt(textData
.length
- 1)) {
1993 textData
= textData
.slice(0, -1);
1996 textData
= textData
.replace("\r\n", "\n");
1998 this.dispatchEvent(new CustomEvent(
2000 { detail
: { text
: textData
} }));
2003 return this._fail("Unexpected action in extended clipboard message: " + actions
);
2009 _handleServerFenceMsg() {
2010 if (this._sock
.rQwait("ServerFence header", 8, 1)) { return false; }
2011 this._sock
.rQskipBytes(3); // Padding
2012 let flags
= this._sock
.rQshift32();
2013 let length
= this._sock
.rQshift8();
2015 if (this._sock
.rQwait("ServerFence payload", length
, 9)) { return false; }
2018 Log
.Warn("Bad payload length (" + length
+ ") in fence response");
2022 const payload
= this._sock
.rQshiftStr(length
);
2024 this._supportsFence
= true;
2029 * (1<<0) - BlockBefore
2030 * (1<<1) - BlockAfter
2035 if (!(flags
& (1<<31))) {
2036 return this._fail("Unexpected fence response");
2039 // Filter out unsupported flags
2040 // FIXME: support syncNext
2041 flags
&= (1<<0) | (1<<1);
2043 // BlockBefore and BlockAfter are automatically handled by
2044 // the fact that we process each incoming message
2046 RFB
.messages
.clientFence(this._sock
, flags
, payload
);
2052 if (this._sock
.rQwait("XVP version and message", 3, 1)) { return false; }
2053 this._sock
.rQskipBytes(1); // Padding
2054 const xvpVer
= this._sock
.rQshift8();
2055 const xvpMsg
= this._sock
.rQshift8();
2059 Log
.Error("XVP Operation Failed");
2062 this._rfbXvpVer
= xvpVer
;
2063 Log
.Info("XVP extensions enabled (version " + this._rfbXvpVer
+ ")");
2064 this._setCapability("power", true);
2067 this._fail("Illegal server XVP message (msg: " + xvpMsg
+ ")");
2076 if (this._FBU
.rects
> 0) {
2079 msgType
= this._sock
.rQshift8();
2084 case 0: // FramebufferUpdate
2085 ret
= this._framebufferUpdate();
2086 if (ret
&& !this._enabledContinuousUpdates
) {
2087 RFB
.messages
.fbUpdateRequest(this._sock
, true, 0, 0,
2088 this._fbWidth
, this._fbHeight
);
2092 case 1: // SetColorMapEntries
2093 return this._handleSetColourMapMsg();
2097 this.dispatchEvent(new CustomEvent(
2102 case 3: // ServerCutText
2103 return this._handleServerCutText();
2105 case 150: // EndOfContinuousUpdates
2106 first
= !this._supportsContinuousUpdates
;
2107 this._supportsContinuousUpdates
= true;
2108 this._enabledContinuousUpdates
= false;
2110 this._enabledContinuousUpdates
= true;
2111 this._updateContinuousUpdates();
2112 Log
.Info("Enabling continuous updates.");
2114 // FIXME: We need to send a framebufferupdaterequest here
2115 // if we add support for turning off continuous updates
2119 case 248: // ServerFence
2120 return this._handleServerFenceMsg();
2123 return this._handleXvpMsg();
2126 this._fail("Unexpected server message (type " + msgType
+ ")");
2127 Log
.Debug("sock.rQslice(0, 30): " + this._sock
.rQslice(0, 30));
2133 this._flushing
= false;
2134 // Resume processing
2135 if (this._sock
.rQlen
> 0) {
2136 this._handleMessage();
2140 _framebufferUpdate() {
2141 if (this._FBU
.rects
=== 0) {
2142 if (this._sock
.rQwait("FBU header", 3, 1)) { return false; }
2143 this._sock
.rQskipBytes(1); // Padding
2144 this._FBU
.rects
= this._sock
.rQshift16();
2146 // Make sure the previous frame is fully rendered first
2147 // to avoid building up an excessive queue
2148 if (this._display
.pending()) {
2149 this._flushing
= true;
2150 this._display
.flush();
2155 while (this._FBU
.rects
> 0) {
2156 if (this._FBU
.encoding
=== null) {
2157 if (this._sock
.rQwait("rect header", 12)) { return false; }
2158 /* New FramebufferUpdate */
2160 const hdr
= this._sock
.rQshiftBytes(12);
2161 this._FBU
.x
= (hdr
[0] << 8) + hdr
[1];
2162 this._FBU
.y
= (hdr
[2] << 8) + hdr
[3];
2163 this._FBU
.width
= (hdr
[4] << 8) + hdr
[5];
2164 this._FBU
.height
= (hdr
[6] << 8) + hdr
[7];
2165 this._FBU
.encoding
= parseInt((hdr
[8] << 24) + (hdr
[9] << 16) +
2166 (hdr
[10] << 8) + hdr
[11], 10);
2169 if (!this._handleRect()) {
2174 this._FBU
.encoding
= null;
2177 this._display
.flip();
2179 return true; // We finished this FBU
2183 switch (this._FBU
.encoding
) {
2184 case encodings
.pseudoEncodingLastRect
:
2185 this._FBU
.rects
= 1; // Will be decreased when we return
2188 case encodings
.pseudoEncodingVMwareCursor
:
2189 return this._handleVMwareCursor();
2191 case encodings
.pseudoEncodingCursor
:
2192 return this._handleCursor();
2194 case encodings
.pseudoEncodingQEMUExtendedKeyEvent
:
2195 this._qemuExtKeyEventSupported
= true;
2198 case encodings
.pseudoEncodingDesktopName
:
2199 return this._handleDesktopName();
2201 case encodings
.pseudoEncodingDesktopSize
:
2202 this._resize(this._FBU
.width
, this._FBU
.height
);
2205 case encodings
.pseudoEncodingExtendedDesktopSize
:
2206 return this._handleExtendedDesktopSize();
2209 return this._handleDataRect();
2213 _handleVMwareCursor() {
2214 const hotx
= this._FBU
.x
; // hotspot-x
2215 const hoty
= this._FBU
.y
; // hotspot-y
2216 const w
= this._FBU
.width
;
2217 const h
= this._FBU
.height
;
2218 if (this._sock
.rQwait("VMware cursor encoding", 1)) {
2222 const cursorType
= this._sock
.rQshift8();
2224 this._sock
.rQshift8(); //Padding
2227 const bytesPerPixel
= 4;
2230 if (cursorType
== 0) {
2231 //Used to filter away unimportant bits.
2232 //OR is used for correct conversion in js.
2233 const PIXEL_MASK
= 0xffffff00 | 0;
2234 rgba
= new Array(w
* h
* bytesPerPixel
);
2236 if (this._sock
.rQwait("VMware cursor classic encoding",
2237 (w
* h
* bytesPerPixel
) * 2, 2)) {
2241 let andMask
= new Array(w
* h
);
2242 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2243 andMask
[pixel
] = this._sock
.rQshift32();
2246 let xorMask
= new Array(w
* h
);
2247 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2248 xorMask
[pixel
] = this._sock
.rQshift32();
2251 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2252 if (andMask
[pixel
] == 0) {
2253 //Fully opaque pixel
2254 let bgr
= xorMask
[pixel
];
2255 let r
= bgr
>> 8 & 0xff;
2256 let g
= bgr
>> 16 & 0xff;
2257 let b
= bgr
>> 24 & 0xff;
2259 rgba
[(pixel
* bytesPerPixel
) ] = r
; //r
2260 rgba
[(pixel
* bytesPerPixel
) + 1 ] = g
; //g
2261 rgba
[(pixel
* bytesPerPixel
) + 2 ] = b
; //b
2262 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff; //a
2264 } else if ((andMask
[pixel
] & PIXEL_MASK
) ==
2266 //Only screen value matters, no mouse colouring
2267 if (xorMask
[pixel
] == 0) {
2269 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2270 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2271 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2272 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0x00;
2274 } else if ((xorMask
[pixel
] & PIXEL_MASK
) ==
2276 //Inverted pixel, not supported in browsers.
2277 //Fully opaque instead.
2278 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2279 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2280 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2281 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2285 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2286 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2287 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2288 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2293 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2294 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2295 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2296 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2301 } else if (cursorType
== 1) {
2302 if (this._sock
.rQwait("VMware cursor alpha encoding",
2307 rgba
= new Array(w
* h
* bytesPerPixel
);
2309 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2310 let data
= this._sock
.rQshift32();
2312 rgba
[(pixel
* 4) ] = data
>> 24 & 0xff; //r
2313 rgba
[(pixel
* 4) + 1 ] = data
>> 16 & 0xff; //g
2314 rgba
[(pixel
* 4) + 2 ] = data
>> 8 & 0xff; //b
2315 rgba
[(pixel
* 4) + 3 ] = data
& 0xff; //a
2319 Log
.Warn("The given cursor type is not supported: "
2320 + cursorType
+ " given.");
2324 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
2330 const hotx
= this._FBU
.x
; // hotspot-x
2331 const hoty
= this._FBU
.y
; // hotspot-y
2332 const w
= this._FBU
.width
;
2333 const h
= this._FBU
.height
;
2335 const pixelslength
= w
* h
* 4;
2336 const masklength
= Math
.ceil(w
/ 8) * h
;
2338 let bytes
= pixelslength
+ masklength
;
2339 if (this._sock
.rQwait("cursor encoding", bytes
)) {
2343 // Decode from BGRX pixels + bit mask to RGBA
2344 const pixels
= this._sock
.rQshiftBytes(pixelslength
);
2345 const mask
= this._sock
.rQshiftBytes(masklength
);
2346 let rgba
= new Uint8Array(w
* h
* 4);
2349 for (let y
= 0; y
< h
; y
++) {
2350 for (let x
= 0; x
< w
; x
++) {
2351 let maskIdx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/ 8);
2352 let alpha
= (mask
[maskIdx
] << (x
% 8)) & 0x80 ? 255 : 0;
2353 rgba
[pixIdx
] = pixels
[pixIdx
+ 2];
2354 rgba
[pixIdx
+ 1] = pixels
[pixIdx
+ 1];
2355 rgba
[pixIdx
+ 2] = pixels
[pixIdx
];
2356 rgba
[pixIdx
+ 3] = alpha
;
2361 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
2366 _handleDesktopName() {
2367 if (this._sock
.rQwait("DesktopName", 4)) {
2371 let length
= this._sock
.rQshift32();
2373 if (this._sock
.rQwait("DesktopName", length
, 4)) {
2377 let name
= this._sock
.rQshiftStr(length
);
2378 name
= decodeUTF8(name
, true);
2380 this._setDesktopName(name
);
2385 _handleExtendedDesktopSize() {
2386 if (this._sock
.rQwait("ExtendedDesktopSize", 4)) {
2390 const numberOfScreens
= this._sock
.rQpeek8();
2392 let bytes
= 4 + (numberOfScreens
* 16);
2393 if (this._sock
.rQwait("ExtendedDesktopSize", bytes
)) {
2397 const firstUpdate
= !this._supportsSetDesktopSize
;
2398 this._supportsSetDesktopSize
= true;
2400 // Normally we only apply the current resize mode after a
2401 // window resize event. However there is no such trigger on the
2402 // initial connect. And we don't know if the server supports
2403 // resizing until we've gotten here.
2405 this._requestRemoteResize();
2408 this._sock
.rQskipBytes(1); // number-of-screens
2409 this._sock
.rQskipBytes(3); // padding
2411 for (let i
= 0; i
< numberOfScreens
; i
+= 1) {
2412 // Save the id and flags of the first screen
2414 this._screenID
= this._sock
.rQshiftBytes(4); // id
2415 this._sock
.rQskipBytes(2); // x-position
2416 this._sock
.rQskipBytes(2); // y-position
2417 this._sock
.rQskipBytes(2); // width
2418 this._sock
.rQskipBytes(2); // height
2419 this._screenFlags
= this._sock
.rQshiftBytes(4); // flags
2421 this._sock
.rQskipBytes(16);
2426 * The x-position indicates the reason for the change:
2428 * 0 - server resized on its own
2429 * 1 - this client requested the resize
2430 * 2 - another client requested the resize
2433 // We need to handle errors when we requested the resize.
2434 if (this._FBU
.x
=== 1 && this._FBU
.y
!== 0) {
2436 // The y-position indicates the status code from the server
2437 switch (this._FBU
.y
) {
2439 msg
= "Resize is administratively prohibited";
2442 msg
= "Out of resources";
2445 msg
= "Invalid screen layout";
2448 msg
= "Unknown reason";
2451 Log
.Warn("Server did not accept the resize request: "
2454 this._resize(this._FBU
.width
, this._FBU
.height
);
2461 let decoder
= this._decoders
[this._FBU
.encoding
];
2463 this._fail("Unsupported encoding (encoding: " +
2464 this._FBU
.encoding
+ ")");
2469 return decoder
.decodeRect(this._FBU
.x
, this._FBU
.y
,
2470 this._FBU
.width
, this._FBU
.height
,
2471 this._sock
, this._display
,
2474 this._fail("Error decoding rect: " + err
);
2479 _updateContinuousUpdates() {
2480 if (!this._enabledContinuousUpdates
) { return; }
2482 RFB
.messages
.enableContinuousUpdates(this._sock
, true, 0, 0,
2483 this._fbWidth
, this._fbHeight
);
2486 _resize(width
, height
) {
2487 this._fbWidth
= width
;
2488 this._fbHeight
= height
;
2490 this._display
.resize(this._fbWidth
, this._fbHeight
);
2492 // Adjust the visible viewport based on the new dimensions
2494 this._updateScale();
2496 this._updateContinuousUpdates();
2500 if (this._rfbXvpVer
< ver
) { return; }
2501 Log
.Info("Sending XVP operation " + op
+ " (version " + ver
+ ")");
2502 RFB
.messages
.xvpOp(this._sock
, ver
, op
);
2505 _updateCursor(rgba
, hotx
, hoty
, w
, h
) {
2506 this._cursorImage
= {
2508 hotx
: hotx
, hoty
: hoty
, w
: w
, h
: h
,
2510 this._refreshCursor();
2513 _shouldShowDotCursor() {
2514 // Called when this._cursorImage is updated
2515 if (!this._showDotCursor
) {
2516 // User does not want to see the dot, so...
2520 // The dot should not be shown if the cursor is already visible,
2521 // i.e. contains at least one not-fully-transparent pixel.
2522 // So iterate through all alpha bytes in rgba and stop at the
2524 for (let i
= 3; i
< this._cursorImage
.rgbaPixels
.length
; i
+= 4) {
2525 if (this._cursorImage
.rgbaPixels
[i
]) {
2530 // At this point, we know that the cursor is fully transparent, and
2531 // the user wants to see the dot instead of this.
2536 if (this._rfbConnectionState
!== "connecting" &&
2537 this._rfbConnectionState
!== "connected") {
2540 const image
= this._shouldShowDotCursor() ? RFB
.cursors
.dot
: this._cursorImage
;
2541 this._cursor
.change(image
.rgbaPixels
,
2542 image
.hotx
, image
.hoty
,
2547 static genDES(password
, challenge
) {
2548 const passwordChars
= password
.split('').map(c
=> c
.charCodeAt(0));
2549 return (new DES(passwordChars
)).encrypt(challenge
);
2555 keyEvent(sock
, keysym
, down
) {
2556 const buff
= sock
._sQ
;
2557 const offset
= sock
._sQlen
;
2559 buff
[offset
] = 4; // msg-type
2560 buff
[offset
+ 1] = down
;
2562 buff
[offset
+ 2] = 0;
2563 buff
[offset
+ 3] = 0;
2565 buff
[offset
+ 4] = (keysym
>> 24);
2566 buff
[offset
+ 5] = (keysym
>> 16);
2567 buff
[offset
+ 6] = (keysym
>> 8);
2568 buff
[offset
+ 7] = keysym
;
2574 QEMUExtendedKeyEvent(sock
, keysym
, down
, keycode
) {
2575 function getRFBkeycode(xtScanCode
) {
2576 const upperByte
= (keycode
>> 8);
2577 const lowerByte
= (keycode
& 0x00ff);
2578 if (upperByte
=== 0xe0 && lowerByte
< 0x7f) {
2579 return lowerByte
| 0x80;
2584 const buff
= sock
._sQ
;
2585 const offset
= sock
._sQlen
;
2587 buff
[offset
] = 255; // msg-type
2588 buff
[offset
+ 1] = 0; // sub msg-type
2590 buff
[offset
+ 2] = (down
>> 8);
2591 buff
[offset
+ 3] = down
;
2593 buff
[offset
+ 4] = (keysym
>> 24);
2594 buff
[offset
+ 5] = (keysym
>> 16);
2595 buff
[offset
+ 6] = (keysym
>> 8);
2596 buff
[offset
+ 7] = keysym
;
2598 const RFBkeycode
= getRFBkeycode(keycode
);
2600 buff
[offset
+ 8] = (RFBkeycode
>> 24);
2601 buff
[offset
+ 9] = (RFBkeycode
>> 16);
2602 buff
[offset
+ 10] = (RFBkeycode
>> 8);
2603 buff
[offset
+ 11] = RFBkeycode
;
2609 pointerEvent(sock
, x
, y
, mask
) {
2610 const buff
= sock
._sQ
;
2611 const offset
= sock
._sQlen
;
2613 buff
[offset
] = 5; // msg-type
2615 buff
[offset
+ 1] = mask
;
2617 buff
[offset
+ 2] = x
>> 8;
2618 buff
[offset
+ 3] = x
;
2620 buff
[offset
+ 4] = y
>> 8;
2621 buff
[offset
+ 5] = y
;
2627 // Used to build Notify and Request data.
2628 _buildExtendedClipboardFlags(actions
, formats
) {
2629 let data
= new Uint8Array(4);
2630 let formatFlag
= 0x00000000;
2631 let actionFlag
= 0x00000000;
2633 for (let i
= 0; i
< actions
.length
; i
++) {
2634 actionFlag
|= actions
[i
];
2637 for (let i
= 0; i
< formats
.length
; i
++) {
2638 formatFlag
|= formats
[i
];
2641 data
[0] = actionFlag
>> 24; // Actions
2642 data
[1] = 0x00; // Reserved
2643 data
[2] = 0x00; // Reserved
2644 data
[3] = formatFlag
; // Formats
2649 extendedClipboardProvide(sock
, formats
, inData
) {
2650 // Deflate incomming data and their sizes
2651 let deflator
= new Deflator();
2652 let dataToDeflate
= [];
2654 for (let i
= 0; i
< formats
.length
; i
++) {
2655 // We only support the format Text at this time
2656 if (formats
[i
] != extendedClipboardFormatText
) {
2657 throw new Error("Unsupported extended clipboard format for Provide message.");
2660 // Change lone \r or \n into \r\n as defined in rfbproto
2661 inData
[i
] = inData
[i
].replace(/\r\n|\r|\n/gm, "\r\n");
2663 // Check if it already has \0
2664 let text
= encodeUTF8(inData
[i
] + "\0");
2666 dataToDeflate
.push( (text
.length
>> 24) & 0xFF,
2667 (text
.length
>> 16) & 0xFF,
2668 (text
.length
>> 8) & 0xFF,
2669 (text
.length
& 0xFF));
2671 for (let j
= 0; j
< text
.length
; j
++) {
2672 dataToDeflate
.push(text
.charCodeAt(j
));
2676 let deflatedData
= deflator
.deflate(new Uint8Array(dataToDeflate
));
2678 // Build data to send
2679 let data
= new Uint8Array(4 + deflatedData
.length
);
2680 data
.set(RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionProvide
],
2682 data
.set(deflatedData
, 4);
2684 RFB
.messages
.clientCutText(sock
, data
, true);
2687 extendedClipboardNotify(sock
, formats
) {
2688 let flags
= RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionNotify
],
2690 RFB
.messages
.clientCutText(sock
, flags
, true);
2693 extendedClipboardRequest(sock
, formats
) {
2694 let flags
= RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionRequest
],
2696 RFB
.messages
.clientCutText(sock
, flags
, true);
2699 extendedClipboardCaps(sock
, actions
, formats
) {
2700 let formatKeys
= Object
.keys(formats
);
2701 let data
= new Uint8Array(4 + (4 * formatKeys
.length
));
2703 formatKeys
.map(x
=> parseInt(x
));
2704 formatKeys
.sort((a
, b
) => a
- b
);
2706 data
.set(RFB
.messages
._buildExtendedClipboardFlags(actions
, []));
2709 for (let i
= 0; i
< formatKeys
.length
; i
++) {
2710 data
[loopOffset
] = formats
[formatKeys
[i
]] >> 24;
2711 data
[loopOffset
+ 1] = formats
[formatKeys
[i
]] >> 16;
2712 data
[loopOffset
+ 2] = formats
[formatKeys
[i
]] >> 8;
2713 data
[loopOffset
+ 3] = formats
[formatKeys
[i
]] >> 0;
2716 data
[3] |= (1 << formatKeys
[i
]); // Update our format flags
2719 RFB
.messages
.clientCutText(sock
, data
, true);
2722 clientCutText(sock
, data
, extended
= false) {
2723 const buff
= sock
._sQ
;
2724 const offset
= sock
._sQlen
;
2726 buff
[offset
] = 6; // msg-type
2728 buff
[offset
+ 1] = 0; // padding
2729 buff
[offset
+ 2] = 0; // padding
2730 buff
[offset
+ 3] = 0; // padding
2734 length
= toUnsigned32bit(-data
.length
);
2736 length
= data
.length
;
2739 buff
[offset
+ 4] = length
>> 24;
2740 buff
[offset
+ 5] = length
>> 16;
2741 buff
[offset
+ 6] = length
>> 8;
2742 buff
[offset
+ 7] = length
;
2746 // We have to keep track of from where in the data we begin creating the
2747 // buffer for the flush in the next iteration.
2750 let remaining
= data
.length
;
2751 while (remaining
> 0) {
2753 let flushSize
= Math
.min(remaining
, (sock
._sQbufferSize
- sock
._sQlen
));
2754 for (let i
= 0; i
< flushSize
; i
++) {
2755 buff
[sock
._sQlen
+ i
] = data
[dataOffset
+ i
];
2758 sock
._sQlen
+= flushSize
;
2761 remaining
-= flushSize
;
2762 dataOffset
+= flushSize
;
2767 setDesktopSize(sock
, width
, height
, id
, flags
) {
2768 const buff
= sock
._sQ
;
2769 const offset
= sock
._sQlen
;
2771 buff
[offset
] = 251; // msg-type
2772 buff
[offset
+ 1] = 0; // padding
2773 buff
[offset
+ 2] = width
>> 8; // width
2774 buff
[offset
+ 3] = width
;
2775 buff
[offset
+ 4] = height
>> 8; // height
2776 buff
[offset
+ 5] = height
;
2778 buff
[offset
+ 6] = 1; // number-of-screens
2779 buff
[offset
+ 7] = 0; // padding
2782 buff
[offset
+ 8] = id
>> 24; // id
2783 buff
[offset
+ 9] = id
>> 16;
2784 buff
[offset
+ 10] = id
>> 8;
2785 buff
[offset
+ 11] = id
;
2786 buff
[offset
+ 12] = 0; // x-position
2787 buff
[offset
+ 13] = 0;
2788 buff
[offset
+ 14] = 0; // y-position
2789 buff
[offset
+ 15] = 0;
2790 buff
[offset
+ 16] = width
>> 8; // width
2791 buff
[offset
+ 17] = width
;
2792 buff
[offset
+ 18] = height
>> 8; // height
2793 buff
[offset
+ 19] = height
;
2794 buff
[offset
+ 20] = flags
>> 24; // flags
2795 buff
[offset
+ 21] = flags
>> 16;
2796 buff
[offset
+ 22] = flags
>> 8;
2797 buff
[offset
+ 23] = flags
;
2803 clientFence(sock
, flags
, payload
) {
2804 const buff
= sock
._sQ
;
2805 const offset
= sock
._sQlen
;
2807 buff
[offset
] = 248; // msg-type
2809 buff
[offset
+ 1] = 0; // padding
2810 buff
[offset
+ 2] = 0; // padding
2811 buff
[offset
+ 3] = 0; // padding
2813 buff
[offset
+ 4] = flags
>> 24; // flags
2814 buff
[offset
+ 5] = flags
>> 16;
2815 buff
[offset
+ 6] = flags
>> 8;
2816 buff
[offset
+ 7] = flags
;
2818 const n
= payload
.length
;
2820 buff
[offset
+ 8] = n
; // length
2822 for (let i
= 0; i
< n
; i
++) {
2823 buff
[offset
+ 9 + i
] = payload
.charCodeAt(i
);
2826 sock
._sQlen
+= 9 + n
;
2830 enableContinuousUpdates(sock
, enable
, x
, y
, width
, height
) {
2831 const buff
= sock
._sQ
;
2832 const offset
= sock
._sQlen
;
2834 buff
[offset
] = 150; // msg-type
2835 buff
[offset
+ 1] = enable
; // enable-flag
2837 buff
[offset
+ 2] = x
>> 8; // x
2838 buff
[offset
+ 3] = x
;
2839 buff
[offset
+ 4] = y
>> 8; // y
2840 buff
[offset
+ 5] = y
;
2841 buff
[offset
+ 6] = width
>> 8; // width
2842 buff
[offset
+ 7] = width
;
2843 buff
[offset
+ 8] = height
>> 8; // height
2844 buff
[offset
+ 9] = height
;
2850 pixelFormat(sock
, depth
, trueColor
) {
2851 const buff
= sock
._sQ
;
2852 const offset
= sock
._sQlen
;
2858 } else if (depth
> 8) {
2864 const bits
= Math
.floor(depth
/3);
2866 buff
[offset
] = 0; // msg-type
2868 buff
[offset
+ 1] = 0; // padding
2869 buff
[offset
+ 2] = 0; // padding
2870 buff
[offset
+ 3] = 0; // padding
2872 buff
[offset
+ 4] = bpp
; // bits-per-pixel
2873 buff
[offset
+ 5] = depth
; // depth
2874 buff
[offset
+ 6] = 0; // little-endian
2875 buff
[offset
+ 7] = trueColor
? 1 : 0; // true-color
2877 buff
[offset
+ 8] = 0; // red-max
2878 buff
[offset
+ 9] = (1 << bits
) - 1; // red-max
2880 buff
[offset
+ 10] = 0; // green-max
2881 buff
[offset
+ 11] = (1 << bits
) - 1; // green-max
2883 buff
[offset
+ 12] = 0; // blue-max
2884 buff
[offset
+ 13] = (1 << bits
) - 1; // blue-max
2886 buff
[offset
+ 14] = bits
* 0; // red-shift
2887 buff
[offset
+ 15] = bits
* 1; // green-shift
2888 buff
[offset
+ 16] = bits
* 2; // blue-shift
2890 buff
[offset
+ 17] = 0; // padding
2891 buff
[offset
+ 18] = 0; // padding
2892 buff
[offset
+ 19] = 0; // padding
2898 clientEncodings(sock
, encodings
) {
2899 const buff
= sock
._sQ
;
2900 const offset
= sock
._sQlen
;
2902 buff
[offset
] = 2; // msg-type
2903 buff
[offset
+ 1] = 0; // padding
2905 buff
[offset
+ 2] = encodings
.length
>> 8;
2906 buff
[offset
+ 3] = encodings
.length
;
2909 for (let i
= 0; i
< encodings
.length
; i
++) {
2910 const enc
= encodings
[i
];
2911 buff
[j
] = enc
>> 24;
2912 buff
[j
+ 1] = enc
>> 16;
2913 buff
[j
+ 2] = enc
>> 8;
2919 sock
._sQlen
+= j
- offset
;
2923 fbUpdateRequest(sock
, incremental
, x
, y
, w
, h
) {
2924 const buff
= sock
._sQ
;
2925 const offset
= sock
._sQlen
;
2927 if (typeof(x
) === "undefined") { x
= 0; }
2928 if (typeof(y
) === "undefined") { y
= 0; }
2930 buff
[offset
] = 3; // msg-type
2931 buff
[offset
+ 1] = incremental
? 1 : 0;
2933 buff
[offset
+ 2] = (x
>> 8) & 0xFF;
2934 buff
[offset
+ 3] = x
& 0xFF;
2936 buff
[offset
+ 4] = (y
>> 8) & 0xFF;
2937 buff
[offset
+ 5] = y
& 0xFF;
2939 buff
[offset
+ 6] = (w
>> 8) & 0xFF;
2940 buff
[offset
+ 7] = w
& 0xFF;
2942 buff
[offset
+ 8] = (h
>> 8) & 0xFF;
2943 buff
[offset
+ 9] = h
& 0xFF;
2949 xvpOp(sock
, ver
, op
) {
2950 const buff
= sock
._sQ
;
2951 const offset
= sock
._sQlen
;
2953 buff
[offset
] = 250; // msg-type
2954 buff
[offset
+ 1] = 0; // padding
2956 buff
[offset
+ 2] = ver
;
2957 buff
[offset
+ 3] = op
;
2966 rgbaPixels
: new Uint8Array(),
2972 /* eslint-disable indent */
2973 rgbaPixels
: new Uint8Array([
2974 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2975 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
2976 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2978 /* eslint-enable indent */