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 // This it not possible when a pre-existing socket is passed in and is just opened.
291 // If the caller creates this object in the open() callback of a socket and there's
292 // data pending doing it next tick causes a packet to be lost.
293 // This is particularly noticable for RTCDataChannel's where the other end creates
294 // the channel and the client, this end, gets notified it exists.
295 if (typeof urlOrChannel
=== 'string') {
296 setTimeout(this._updateConnectionState
.bind(this, 'connecting'));
298 this._updateConnectionState('connecting');
301 Log
.Debug("<< RFB.constructor");
303 // ===== PROPERTIES =====
305 this.dragViewport
= false;
306 this.focusOnClick
= true;
308 this._viewOnly
= false;
309 this._clipViewport
= false;
310 this._scaleViewport
= false;
311 this._resizeSession
= false;
313 this._showDotCursor
= false;
314 if (options
.showDotCursor
!== undefined) {
315 Log
.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated");
316 this._showDotCursor
= options
.showDotCursor
;
319 this._qualityLevel
= 6;
320 this._compressionLevel
= 2;
323 // ===== PROPERTIES =====
325 get viewOnly() { return this._viewOnly
; }
326 set viewOnly(viewOnly
) {
327 this._viewOnly
= viewOnly
;
329 if (this._rfbConnectionState
=== "connecting" ||
330 this._rfbConnectionState
=== "connected") {
332 this._keyboard
.ungrab();
334 this._keyboard
.grab();
339 get capabilities() { return this._capabilities
; }
341 get touchButton() { return 0; }
342 set touchButton(button
) { Log
.Warn("Using old API!"); }
344 get clipViewport() { return this._clipViewport
; }
345 set clipViewport(viewport
) {
346 this._clipViewport
= viewport
;
350 get scaleViewport() { return this._scaleViewport
; }
351 set scaleViewport(scale
) {
352 this._scaleViewport
= scale
;
353 // Scaling trumps clipping, so we may need to adjust
354 // clipping when enabling or disabling scaling
355 if (scale
&& this._clipViewport
) {
359 if (!scale
&& this._clipViewport
) {
364 get resizeSession() { return this._resizeSession
; }
365 set resizeSession(resize
) {
366 this._resizeSession
= resize
;
368 this._requestRemoteResize();
372 get showDotCursor() { return this._showDotCursor
; }
373 set showDotCursor(show
) {
374 this._showDotCursor
= show
;
375 this._refreshCursor();
378 get background() { return this._screen
.style
.background
; }
379 set background(cssValue
) { this._screen
.style
.background
= cssValue
; }
382 return this._qualityLevel
;
384 set qualityLevel(qualityLevel
) {
385 if (!Number
.isInteger(qualityLevel
) || qualityLevel
< 0 || qualityLevel
> 9) {
386 Log
.Error("qualityLevel must be an integer between 0 and 9");
390 if (this._qualityLevel
=== qualityLevel
) {
394 this._qualityLevel
= qualityLevel
;
396 if (this._rfbConnectionState
=== 'connected') {
397 this._sendEncodings();
401 get compressionLevel() {
402 return this._compressionLevel
;
404 set compressionLevel(compressionLevel
) {
405 if (!Number
.isInteger(compressionLevel
) || compressionLevel
< 0 || compressionLevel
> 9) {
406 Log
.Error("compressionLevel must be an integer between 0 and 9");
410 if (this._compressionLevel
=== compressionLevel
) {
414 this._compressionLevel
= compressionLevel
;
416 if (this._rfbConnectionState
=== 'connected') {
417 this._sendEncodings();
421 // ===== PUBLIC METHODS =====
424 this._updateConnectionState('disconnecting');
425 this._sock
.off('error');
426 this._sock
.off('message');
427 this._sock
.off('open');
430 sendCredentials(creds
) {
431 this._rfbCredentials
= creds
;
432 setTimeout(this._initMsg
.bind(this), 0);
436 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
437 Log
.Info("Sending Ctrl-Alt-Del");
439 this.sendKey(KeyTable
.XK_Control_L
, "ControlLeft", true);
440 this.sendKey(KeyTable
.XK_Alt_L
, "AltLeft", true);
441 this.sendKey(KeyTable
.XK_Delete
, "Delete", true);
442 this.sendKey(KeyTable
.XK_Delete
, "Delete", false);
443 this.sendKey(KeyTable
.XK_Alt_L
, "AltLeft", false);
444 this.sendKey(KeyTable
.XK_Control_L
, "ControlLeft", false);
459 // Send a key press. If 'down' is not specified then send a down key
460 // followed by an up key.
461 sendKey(keysym
, code
, down
) {
462 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
464 if (down
=== undefined) {
465 this.sendKey(keysym
, code
, true);
466 this.sendKey(keysym
, code
, false);
470 const scancode
= XtScancode
[code
];
472 if (this._qemuExtKeyEventSupported
&& scancode
) {
474 keysym
= keysym
|| 0;
476 Log
.Info("Sending key (" + (down
? "down" : "up") + "): keysym " + keysym
+ ", scancode " + scancode
);
478 RFB
.messages
.QEMUExtendedKeyEvent(this._sock
, keysym
, down
, scancode
);
483 Log
.Info("Sending keysym (" + (down
? "down" : "up") + "): " + keysym
);
484 RFB
.messages
.keyEvent(this._sock
, keysym
, down
? 1 : 0);
489 this._canvas
.focus();
496 clipboardPasteFrom(text
) {
497 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
499 if (this._clipboardServerCapabilitiesFormats
[extendedClipboardFormatText
] &&
500 this._clipboardServerCapabilitiesActions
[extendedClipboardActionNotify
]) {
502 this._clipboardText
= text
;
503 RFB
.messages
.extendedClipboardNotify(this._sock
, [extendedClipboardFormatText
]);
505 let data
= new Uint8Array(text
.length
);
506 for (let i
= 0; i
< text
.length
; i
++) {
507 // FIXME: text can have values outside of Latin1/Uint8
508 data
[i
] = text
.charCodeAt(i
);
511 RFB
.messages
.clientCutText(this._sock
, data
);
515 // ===== PRIVATE METHODS =====
518 Log
.Debug(">> RFB.connect");
522 Log
.Info(`connecting to ${this._url}`);
523 this._sock
.open(this._url
, this._wsProtocols
);
525 if (e
.name
=== 'SyntaxError') {
526 this._fail("Invalid host or port (" + e
+ ")");
528 this._fail("Error when opening socket (" + e
+ ")");
533 Log
.Info(`attaching ${this._rawChannel} to Websock`);
534 this._sock
.attach(this._rawChannel
);
536 this._fail("Error attaching channel (" + e
+ ")");
540 // Make our elements part of the page
541 this._target
.appendChild(this._screen
);
543 this._gestures
.attach(this._canvas
);
545 this._cursor
.attach(this._canvas
);
546 this._refreshCursor();
548 // Monitor size changes of the screen
549 // FIXME: Use ResizeObserver, or hidden overflow
550 window
.addEventListener('resize', this._eventHandlers
.windowResize
);
552 // Always grab focus on some kind of click event
553 this._canvas
.addEventListener("mousedown", this._eventHandlers
.focusCanvas
);
554 this._canvas
.addEventListener("touchstart", this._eventHandlers
.focusCanvas
);
557 this._canvas
.addEventListener('mousedown', this._eventHandlers
.handleMouse
);
558 this._canvas
.addEventListener('mouseup', this._eventHandlers
.handleMouse
);
559 this._canvas
.addEventListener('mousemove', this._eventHandlers
.handleMouse
);
560 // Prevent middle-click pasting (see handler for why we bind to document)
561 this._canvas
.addEventListener('click', this._eventHandlers
.handleMouse
);
562 // preventDefault() on mousedown doesn't stop this event for some
563 // reason so we have to explicitly block it
564 this._canvas
.addEventListener('contextmenu', this._eventHandlers
.handleMouse
);
567 this._canvas
.addEventListener("wheel", this._eventHandlers
.handleWheel
);
570 this._canvas
.addEventListener("gesturestart", this._eventHandlers
.handleGesture
);
571 this._canvas
.addEventListener("gesturemove", this._eventHandlers
.handleGesture
);
572 this._canvas
.addEventListener("gestureend", this._eventHandlers
.handleGesture
);
574 Log
.Debug("<< RFB.connect");
578 Log
.Debug(">> RFB.disconnect");
579 this._cursor
.detach();
580 this._canvas
.removeEventListener("gesturestart", this._eventHandlers
.handleGesture
);
581 this._canvas
.removeEventListener("gesturemove", this._eventHandlers
.handleGesture
);
582 this._canvas
.removeEventListener("gestureend", this._eventHandlers
.handleGesture
);
583 this._canvas
.removeEventListener("wheel", this._eventHandlers
.handleWheel
);
584 this._canvas
.removeEventListener('mousedown', this._eventHandlers
.handleMouse
);
585 this._canvas
.removeEventListener('mouseup', this._eventHandlers
.handleMouse
);
586 this._canvas
.removeEventListener('mousemove', this._eventHandlers
.handleMouse
);
587 this._canvas
.removeEventListener('click', this._eventHandlers
.handleMouse
);
588 this._canvas
.removeEventListener('contextmenu', this._eventHandlers
.handleMouse
);
589 this._canvas
.removeEventListener("mousedown", this._eventHandlers
.focusCanvas
);
590 this._canvas
.removeEventListener("touchstart", this._eventHandlers
.focusCanvas
);
591 window
.removeEventListener('resize', this._eventHandlers
.windowResize
);
592 this._keyboard
.ungrab();
593 this._gestures
.detach();
596 this._target
.removeChild(this._screen
);
598 if (e
.name
=== 'NotFoundError') {
599 // Some cases where the initial connection fails
600 // can disconnect before the _screen is created
605 clearTimeout(this._resizeTimeout
);
606 clearTimeout(this._mouseMoveTimer
);
607 Log
.Debug("<< RFB.disconnect");
610 _focusCanvas(event
) {
611 if (!this.focusOnClick
) {
618 _setDesktopName(name
) {
620 this.dispatchEvent(new CustomEvent(
622 { detail
: { name
: this._fbName
} }));
625 _windowResize(event
) {
626 // If the window resized then our screen element might have
627 // as well. Update the viewport dimensions.
628 window
.requestAnimationFrame(() => {
633 if (this._resizeSession
) {
634 // Request changing the resolution of the remote display to
635 // the size of the local browser viewport.
637 // In order to not send multiple requests before the browser-resize
638 // is finished we wait 0.5 seconds before sending the request.
639 clearTimeout(this._resizeTimeout
);
640 this._resizeTimeout
= setTimeout(this._requestRemoteResize
.bind(this), 500);
644 // Update state of clipping in Display object, and make sure the
645 // configured viewport matches the current screen size
647 const curClip
= this._display
.clipViewport
;
648 let newClip
= this._clipViewport
;
650 if (this._scaleViewport
) {
651 // Disable viewport clipping if we are scaling
655 if (curClip
!== newClip
) {
656 this._display
.clipViewport
= newClip
;
660 // When clipping is enabled, the screen is limited to
661 // the size of the container.
662 const size
= this._screenSize();
663 this._display
.viewportChangeSize(size
.w
, size
.h
);
664 this._fixScrollbars();
669 if (!this._scaleViewport
) {
670 this._display
.scale
= 1.0;
672 const size
= this._screenSize();
673 this._display
.autoscale(size
.w
, size
.h
);
675 this._fixScrollbars();
678 // Requests a change of remote desktop size. This message is an extension
679 // and may only be sent if we have received an ExtendedDesktopSize message
680 _requestRemoteResize() {
681 clearTimeout(this._resizeTimeout
);
682 this._resizeTimeout
= null;
684 if (!this._resizeSession
|| this._viewOnly
||
685 !this._supportsSetDesktopSize
) {
689 const size
= this._screenSize();
690 RFB
.messages
.setDesktopSize(this._sock
,
691 Math
.floor(size
.w
), Math
.floor(size
.h
),
692 this._screenID
, this._screenFlags
);
694 Log
.Debug('Requested new desktop size: ' +
695 size
.w
+ 'x' + size
.h
);
698 // Gets the the size of the available screen
700 let r
= this._screen
.getBoundingClientRect();
701 return { w
: r
.width
, h
: r
.height
};
705 // This is a hack because Chrome screws up the calculation
706 // for when scrollbars are needed. So to fix it we temporarily
707 // toggle them off and on.
708 const orig
= this._screen
.style
.overflow
;
709 this._screen
.style
.overflow
= 'hidden';
710 // Force Chrome to recalculate the layout by asking for
711 // an element's dimensions
712 this._screen
.getBoundingClientRect();
713 this._screen
.style
.overflow
= orig
;
721 * disconnected - permanent state
723 _updateConnectionState(state
) {
724 const oldstate
= this._rfbConnectionState
;
726 if (state
=== oldstate
) {
727 Log
.Debug("Already in state '" + state
+ "', ignoring");
731 // The 'disconnected' state is permanent for each RFB object
732 if (oldstate
=== 'disconnected') {
733 Log
.Error("Tried changing state of a disconnected RFB object");
737 // Ensure proper transitions before doing anything
740 if (oldstate
!== 'connecting') {
741 Log
.Error("Bad transition to connected state, " +
742 "previous connection state: " + oldstate
);
748 if (oldstate
!== 'disconnecting') {
749 Log
.Error("Bad transition to disconnected state, " +
750 "previous connection state: " + oldstate
);
756 if (oldstate
!== '') {
757 Log
.Error("Bad transition to connecting state, " +
758 "previous connection state: " + oldstate
);
763 case 'disconnecting':
764 if (oldstate
!== 'connected' && oldstate
!== 'connecting') {
765 Log
.Error("Bad transition to disconnecting state, " +
766 "previous connection state: " + oldstate
);
772 Log
.Error("Unknown connection state: " + state
);
776 // State change actions
778 this._rfbConnectionState
= state
;
780 Log
.Debug("New state '" + state
+ "', was '" + oldstate
+ "'.");
782 if (this._disconnTimer
&& state
!== 'disconnecting') {
783 Log
.Debug("Clearing disconnect timer");
784 clearTimeout(this._disconnTimer
);
785 this._disconnTimer
= null;
787 // make sure we don't get a double event
788 this._sock
.off('close');
797 this.dispatchEvent(new CustomEvent("connect", { detail
: {} }));
800 case 'disconnecting':
803 this._disconnTimer
= setTimeout(() => {
804 Log
.Error("Disconnection timed out.");
805 this._updateConnectionState('disconnected');
806 }, DISCONNECT_TIMEOUT
* 1000);
810 this.dispatchEvent(new CustomEvent(
811 "disconnect", { detail
:
812 { clean
: this._rfbCleanDisconnect
} }));
817 /* Print errors and disconnect
819 * The parameter 'details' is used for information that
820 * should be logged but not sent to the user interface.
823 switch (this._rfbConnectionState
) {
824 case 'disconnecting':
825 Log
.Error("Failed when disconnecting: " + details
);
828 Log
.Error("Failed while connected: " + details
);
831 Log
.Error("Failed when connecting: " + details
);
834 Log
.Error("RFB failure: " + details
);
837 this._rfbCleanDisconnect
= false; //This is sent to the UI
839 // Transition to disconnected without waiting for socket to close
840 this._updateConnectionState('disconnecting');
841 this._updateConnectionState('disconnected');
846 _setCapability(cap
, val
) {
847 this._capabilities
[cap
] = val
;
848 this.dispatchEvent(new CustomEvent("capabilities",
849 { detail
: { capabilities
: this._capabilities
} }));
853 if (this._sock
.rQlen
=== 0) {
854 Log
.Warn("handleMessage called on an empty receive queue");
858 switch (this._rfbConnectionState
) {
860 Log
.Error("Got data while disconnected");
864 if (this._flushing
) {
867 if (!this._normalMsg()) {
870 if (this._sock
.rQlen
=== 0) {
881 _handleKeyEvent(keysym
, code
, down
) {
882 this.sendKey(keysym
, code
, down
);
887 * We don't check connection status or viewOnly here as the
888 * mouse events might be used to control the viewport
891 if (ev
.type
=== 'click') {
893 * Note: This is only needed for the 'click' event as it fails
894 * to fire properly for the target element so we have
895 * to listen on the document element instead.
897 if (ev
.target
!== this._canvas
) {
902 // FIXME: if we're in view-only and not dragging,
903 // should we stop events?
904 ev
.stopPropagation();
907 if ((ev
.type
=== 'click') || (ev
.type
=== 'contextmenu')) {
911 let pos
= clientToElement(ev
.clientX
, ev
.clientY
,
916 setCapture(this._canvas
);
917 this._handleMouseButton(pos
.x
, pos
.y
,
918 true, 1 << ev
.button
);
921 this._handleMouseButton(pos
.x
, pos
.y
,
922 false, 1 << ev
.button
);
925 this._handleMouseMove(pos
.x
, pos
.y
);
930 _handleMouseButton(x
, y
, down
, bmask
) {
931 if (this.dragViewport
) {
932 if (down
&& !this._viewportDragging
) {
933 this._viewportDragging
= true;
934 this._viewportDragPos
= {'x': x
, 'y': y
};
935 this._viewportHasMoved
= false;
937 // Skip sending mouse events
940 this._viewportDragging
= false;
942 // If we actually performed a drag then we are done
943 // here and should not send any mouse events
944 if (this._viewportHasMoved
) {
948 // Otherwise we treat this as a mouse click event.
949 // Send the button down event here, as the button up
950 // event is sent at the end of this function.
951 this._sendMouse(x
, y
, bmask
);
955 // Flush waiting move event first
956 if (this._mouseMoveTimer
!== null) {
957 clearTimeout(this._mouseMoveTimer
);
958 this._mouseMoveTimer
= null;
959 this._sendMouse(x
, y
, this._mouseButtonMask
);
963 this._mouseButtonMask
|= bmask
;
965 this._mouseButtonMask
&= ~bmask
;
968 this._sendMouse(x
, y
, this._mouseButtonMask
);
971 _handleMouseMove(x
, y
) {
972 if (this._viewportDragging
) {
973 const deltaX
= this._viewportDragPos
.x
- x
;
974 const deltaY
= this._viewportDragPos
.y
- y
;
976 if (this._viewportHasMoved
|| (Math
.abs(deltaX
) > dragThreshold
||
977 Math
.abs(deltaY
) > dragThreshold
)) {
978 this._viewportHasMoved
= true;
980 this._viewportDragPos
= {'x': x
, 'y': y
};
981 this._display
.viewportChangePos(deltaX
, deltaY
);
984 // Skip sending mouse events
988 this._mousePos
= { 'x': x
, 'y': y
};
990 // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms
991 if (this._mouseMoveTimer
== null) {
993 const timeSinceLastMove
= Date
.now() - this._mouseLastMoveTime
;
994 if (timeSinceLastMove
> MOUSE_MOVE_DELAY
) {
995 this._sendMouse(x
, y
, this._mouseButtonMask
);
996 this._mouseLastMoveTime
= Date
.now();
998 // Too soon since the latest move, wait the remaining time
999 this._mouseMoveTimer
= setTimeout(() => {
1000 this._handleDelayedMouseMove();
1001 }, MOUSE_MOVE_DELAY
- timeSinceLastMove
);
1006 _handleDelayedMouseMove() {
1007 this._mouseMoveTimer
= null;
1008 this._sendMouse(this._mousePos
.x
, this._mousePos
.y
,
1009 this._mouseButtonMask
);
1010 this._mouseLastMoveTime
= Date
.now();
1013 _sendMouse(x
, y
, mask
) {
1014 if (this._rfbConnectionState
!== 'connected') { return; }
1015 if (this._viewOnly
) { return; } // View only, skip mouse events
1017 RFB
.messages
.pointerEvent(this._sock
, this._display
.absX(x
),
1018 this._display
.absY(y
), mask
);
1022 if (this._rfbConnectionState
!== 'connected') { return; }
1023 if (this._viewOnly
) { return; } // View only, skip mouse events
1025 ev
.stopPropagation();
1026 ev
.preventDefault();
1028 let pos
= clientToElement(ev
.clientX
, ev
.clientY
,
1034 // Pixel units unless it's non-zero.
1035 // Note that if deltamode is line or page won't matter since we aren't
1036 // sending the mouse wheel delta to the server anyway.
1037 // The difference between pixel and line can be important however since
1038 // we have a threshold that can be smaller than the line height.
1039 if (ev
.deltaMode
!== 0) {
1040 dX
*= WHEEL_LINE_HEIGHT
;
1041 dY
*= WHEEL_LINE_HEIGHT
;
1044 // Mouse wheel events are sent in steps over VNC. This means that the VNC
1045 // protocol can't handle a wheel event with specific distance or speed.
1046 // Therefor, if we get a lot of small mouse wheel events we combine them.
1047 this._accumulatedWheelDeltaX
+= dX
;
1048 this._accumulatedWheelDeltaY
+= dY
;
1050 // Generate a mouse wheel step event when the accumulated delta
1051 // for one of the axes is large enough.
1052 if (Math
.abs(this._accumulatedWheelDeltaX
) >= WHEEL_STEP
) {
1053 if (this._accumulatedWheelDeltaX
< 0) {
1054 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 5);
1055 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 5);
1056 } else if (this._accumulatedWheelDeltaX
> 0) {
1057 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 6);
1058 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 6);
1061 this._accumulatedWheelDeltaX
= 0;
1063 if (Math
.abs(this._accumulatedWheelDeltaY
) >= WHEEL_STEP
) {
1064 if (this._accumulatedWheelDeltaY
< 0) {
1065 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 3);
1066 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 3);
1067 } else if (this._accumulatedWheelDeltaY
> 0) {
1068 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 4);
1069 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 4);
1072 this._accumulatedWheelDeltaY
= 0;
1076 _fakeMouseMove(ev
, elementX
, elementY
) {
1077 this._handleMouseMove(elementX
, elementY
);
1078 this._cursor
.move(ev
.detail
.clientX
, ev
.detail
.clientY
);
1081 _handleTapEvent(ev
, bmask
) {
1082 let pos
= clientToElement(ev
.detail
.clientX
, ev
.detail
.clientY
,
1085 // If the user quickly taps multiple times we assume they meant to
1086 // hit the same spot, so slightly adjust coordinates
1088 if ((this._gestureLastTapTime
!== null) &&
1089 ((Date
.now() - this._gestureLastTapTime
) < DOUBLE_TAP_TIMEOUT
) &&
1090 (this._gestureFirstDoubleTapEv
.detail
.type
=== ev
.detail
.type
)) {
1091 let dx
= this._gestureFirstDoubleTapEv
.detail
.clientX
- ev
.detail
.clientX
;
1092 let dy
= this._gestureFirstDoubleTapEv
.detail
.clientY
- ev
.detail
.clientY
;
1093 let distance
= Math
.hypot(dx
, dy
);
1095 if (distance
< DOUBLE_TAP_THRESHOLD
) {
1096 pos
= clientToElement(this._gestureFirstDoubleTapEv
.detail
.clientX
,
1097 this._gestureFirstDoubleTapEv
.detail
.clientY
,
1100 this._gestureFirstDoubleTapEv
= ev
;
1103 this._gestureFirstDoubleTapEv
= ev
;
1105 this._gestureLastTapTime
= Date
.now();
1107 this._fakeMouseMove(this._gestureFirstDoubleTapEv
, pos
.x
, pos
.y
);
1108 this._handleMouseButton(pos
.x
, pos
.y
, true, bmask
);
1109 this._handleMouseButton(pos
.x
, pos
.y
, false, bmask
);
1112 _handleGesture(ev
) {
1115 let pos
= clientToElement(ev
.detail
.clientX
, ev
.detail
.clientY
,
1118 case 'gesturestart':
1119 switch (ev
.detail
.type
) {
1121 this._handleTapEvent(ev
, 0x1);
1124 this._handleTapEvent(ev
, 0x4);
1127 this._handleTapEvent(ev
, 0x2);
1130 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1131 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x1);
1134 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1135 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x4);
1139 this._gestureLastMagnitudeX
= ev
.detail
.magnitudeX
;
1140 this._gestureLastMagnitudeY
= ev
.detail
.magnitudeY
;
1141 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1144 this._gestureLastMagnitudeX
= Math
.hypot(ev
.detail
.magnitudeX
,
1145 ev
.detail
.magnitudeY
);
1146 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1152 switch (ev
.detail
.type
) {
1159 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1162 // Always scroll in the same position.
1163 // We don't know if the mouse was moved so we need to move it
1165 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1166 while ((ev
.detail
.magnitudeY
- this._gestureLastMagnitudeY
) > GESTURE_SCRLSENS
) {
1167 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x8);
1168 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x8);
1169 this._gestureLastMagnitudeY
+= GESTURE_SCRLSENS
;
1171 while ((ev
.detail
.magnitudeY
- this._gestureLastMagnitudeY
) < -GESTURE_SCRLSENS
) {
1172 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x10);
1173 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x10);
1174 this._gestureLastMagnitudeY
-= GESTURE_SCRLSENS
;
1176 while ((ev
.detail
.magnitudeX
- this._gestureLastMagnitudeX
) > GESTURE_SCRLSENS
) {
1177 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x20);
1178 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x20);
1179 this._gestureLastMagnitudeX
+= GESTURE_SCRLSENS
;
1181 while ((ev
.detail
.magnitudeX
- this._gestureLastMagnitudeX
) < -GESTURE_SCRLSENS
) {
1182 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x40);
1183 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x40);
1184 this._gestureLastMagnitudeX
-= GESTURE_SCRLSENS
;
1188 // Always scroll in the same position.
1189 // We don't know if the mouse was moved so we need to move it
1191 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1192 magnitude
= Math
.hypot(ev
.detail
.magnitudeX
, ev
.detail
.magnitudeY
);
1193 if (Math
.abs(magnitude
- this._gestureLastMagnitudeX
) > GESTURE_ZOOMSENS
) {
1194 this._handleKeyEvent(KeyTable
.XK_Control_L
, "ControlLeft", true);
1195 while ((magnitude
- this._gestureLastMagnitudeX
) > GESTURE_ZOOMSENS
) {
1196 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x8);
1197 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x8);
1198 this._gestureLastMagnitudeX
+= GESTURE_ZOOMSENS
;
1200 while ((magnitude
- this._gestureLastMagnitudeX
) < -GESTURE_ZOOMSENS
) {
1201 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x10);
1202 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x10);
1203 this._gestureLastMagnitudeX
-= GESTURE_ZOOMSENS
;
1206 this._handleKeyEvent(KeyTable
.XK_Control_L
, "ControlLeft", false);
1212 switch (ev
.detail
.type
) {
1220 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1221 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x1);
1224 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1225 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x4);
1234 _negotiateProtocolVersion() {
1235 if (this._sock
.rQwait("version", 12)) {
1239 const sversion
= this._sock
.rQshiftStr(12).substr(4, 7);
1240 Log
.Info("Server ProtocolVersion: " + sversion
);
1243 case "000.000": // UltraVNC repeater
1247 case "003.006": // UltraVNC
1248 case "003.889": // Apple Remote Desktop
1249 this._rfbVersion
= 3.3;
1252 this._rfbVersion
= 3.7;
1255 case "004.000": // Intel AMT KVM
1256 case "004.001": // RealVNC 4.6
1257 case "005.000": // RealVNC 5.3
1258 this._rfbVersion
= 3.8;
1261 return this._fail("Invalid server version " + sversion
);
1265 let repeaterID
= "ID:" + this._repeaterID
;
1266 while (repeaterID
.length
< 250) {
1269 this._sock
.sendString(repeaterID
);
1273 if (this._rfbVersion
> this._rfbMaxVersion
) {
1274 this._rfbVersion
= this._rfbMaxVersion
;
1277 const cversion
= "00" + parseInt(this._rfbVersion
, 10) +
1278 ".00" + ((this._rfbVersion
* 10) % 10);
1279 this._sock
.sendString("RFB " + cversion
+ "\n");
1280 Log
.Debug('Sent ProtocolVersion: ' + cversion
);
1282 this._rfbInitState
= 'Security';
1285 _negotiateSecurity() {
1286 if (this._rfbVersion
>= 3.7) {
1287 // Server sends supported list, client decides
1288 const numTypes
= this._sock
.rQshift8();
1289 if (this._sock
.rQwait("security type", numTypes
, 1)) { return false; }
1291 if (numTypes
=== 0) {
1292 this._rfbInitState
= "SecurityReason";
1293 this._securityContext
= "no security types";
1294 this._securityStatus
= 1;
1295 return this._initMsg();
1298 const types
= this._sock
.rQshiftBytes(numTypes
);
1299 Log
.Debug("Server security types: " + types
);
1301 // Look for each auth in preferred order
1302 if (types
.includes(1)) {
1303 this._rfbAuthScheme
= 1; // None
1304 } else if (types
.includes(22)) {
1305 this._rfbAuthScheme
= 22; // XVP
1306 } else if (types
.includes(16)) {
1307 this._rfbAuthScheme
= 16; // Tight
1308 } else if (types
.includes(2)) {
1309 this._rfbAuthScheme
= 2; // VNC Auth
1310 } else if (types
.includes(19)) {
1311 this._rfbAuthScheme
= 19; // VeNCrypt Auth
1313 return this._fail("Unsupported security types (types: " + types
+ ")");
1316 this._sock
.send([this._rfbAuthScheme
]);
1319 if (this._sock
.rQwait("security scheme", 4)) { return false; }
1320 this._rfbAuthScheme
= this._sock
.rQshift32();
1322 if (this._rfbAuthScheme
== 0) {
1323 this._rfbInitState
= "SecurityReason";
1324 this._securityContext
= "authentication scheme";
1325 this._securityStatus
= 1;
1326 return this._initMsg();
1330 this._rfbInitState
= 'Authentication';
1331 Log
.Debug('Authenticating using scheme: ' + this._rfbAuthScheme
);
1333 return this._initMsg(); // jump to authentication
1336 _handleSecurityReason() {
1337 if (this._sock
.rQwait("reason length", 4)) {
1340 const strlen
= this._sock
.rQshift32();
1344 if (this._sock
.rQwait("reason", strlen
, 4)) { return false; }
1345 reason
= this._sock
.rQshiftStr(strlen
);
1348 if (reason
!== "") {
1349 this.dispatchEvent(new CustomEvent(
1351 { detail
: { status
: this._securityStatus
,
1352 reason
: reason
} }));
1354 return this._fail("Security negotiation failed on " +
1355 this._securityContext
+
1356 " (reason: " + reason
+ ")");
1358 this.dispatchEvent(new CustomEvent(
1360 { detail
: { status
: this._securityStatus
} }));
1362 return this._fail("Security negotiation failed on " +
1363 this._securityContext
);
1368 _negotiateXvpAuth() {
1369 if (this._rfbCredentials
.username
=== undefined ||
1370 this._rfbCredentials
.password
=== undefined ||
1371 this._rfbCredentials
.target
=== undefined) {
1372 this.dispatchEvent(new CustomEvent(
1373 "credentialsrequired",
1374 { detail
: { types
: ["username", "password", "target"] } }));
1378 const xvpAuthStr
= String
.fromCharCode(this._rfbCredentials
.username
.length
) +
1379 String
.fromCharCode(this._rfbCredentials
.target
.length
) +
1380 this._rfbCredentials
.username
+
1381 this._rfbCredentials
.target
;
1382 this._sock
.sendString(xvpAuthStr
);
1383 this._rfbAuthScheme
= 2;
1384 return this._negotiateAuthentication();
1387 // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
1388 _negotiateVeNCryptAuth() {
1390 // waiting for VeNCrypt version
1391 if (this._rfbVeNCryptState
== 0) {
1392 if (this._sock
.rQwait("vencrypt version", 2)) { return false; }
1394 const major
= this._sock
.rQshift8();
1395 const minor
= this._sock
.rQshift8();
1397 if (!(major
== 0 && minor
== 2)) {
1398 return this._fail("Unsupported VeNCrypt version " + major
+ "." + minor
);
1401 this._sock
.send([0, 2]);
1402 this._rfbVeNCryptState
= 1;
1406 if (this._rfbVeNCryptState
== 1) {
1407 if (this._sock
.rQwait("vencrypt ack", 1)) { return false; }
1409 const res
= this._sock
.rQshift8();
1412 return this._fail("VeNCrypt failure " + res
);
1415 this._rfbVeNCryptState
= 2;
1417 // must fall through here (i.e. no "else if"), beacause we may have already received
1418 // the subtypes length and won't be called again
1420 if (this._rfbVeNCryptState
== 2) { // waiting for subtypes length
1421 if (this._sock
.rQwait("vencrypt subtypes length", 1)) { return false; }
1423 const subtypesLength
= this._sock
.rQshift8();
1424 if (subtypesLength
< 1) {
1425 return this._fail("VeNCrypt subtypes empty");
1428 this._rfbVeNCryptSubtypesLength
= subtypesLength
;
1429 this._rfbVeNCryptState
= 3;
1432 // waiting for subtypes list
1433 if (this._rfbVeNCryptState
== 3) {
1434 if (this._sock
.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength
)) { return false; }
1436 const subtypes
= [];
1437 for (let i
= 0; i
< this._rfbVeNCryptSubtypesLength
; i
++) {
1438 subtypes
.push(this._sock
.rQshift32());
1441 // 256 = Plain subtype
1442 if (subtypes
.indexOf(256) != -1) {
1444 this._sock
.send([0, 0, 1, 0]);
1445 this._rfbVeNCryptState
= 4;
1447 return this._fail("VeNCrypt Plain subtype not offered by server");
1451 // negotiated Plain subtype, server waits for password
1452 if (this._rfbVeNCryptState
== 4) {
1453 if (this._rfbCredentials
.username
=== undefined ||
1454 this._rfbCredentials
.password
=== undefined) {
1455 this.dispatchEvent(new CustomEvent(
1456 "credentialsrequired",
1457 { detail
: { types
: ["username", "password"] } }));
1461 const user
= encodeUTF8(this._rfbCredentials
.username
);
1462 const pass
= encodeUTF8(this._rfbCredentials
.password
);
1465 (user
.length
>> 24) & 0xFF,
1466 (user
.length
>> 16) & 0xFF,
1467 (user
.length
>> 8) & 0xFF,
1471 (pass
.length
>> 24) & 0xFF,
1472 (pass
.length
>> 16) & 0xFF,
1473 (pass
.length
>> 8) & 0xFF,
1476 this._sock
.sendString(user
);
1477 this._sock
.sendString(pass
);
1479 this._rfbInitState
= "SecurityResult";
1484 _negotiateStdVNCAuth() {
1485 if (this._sock
.rQwait("auth challenge", 16)) { return false; }
1487 if (this._rfbCredentials
.password
=== undefined) {
1488 this.dispatchEvent(new CustomEvent(
1489 "credentialsrequired",
1490 { detail
: { types
: ["password"] } }));
1494 // TODO(directxman12): make genDES not require an Array
1495 const challenge
= Array
.prototype.slice
.call(this._sock
.rQshiftBytes(16));
1496 const response
= RFB
.genDES(this._rfbCredentials
.password
, challenge
);
1497 this._sock
.send(response
);
1498 this._rfbInitState
= "SecurityResult";
1502 _negotiateTightUnixAuth() {
1503 if (this._rfbCredentials
.username
=== undefined ||
1504 this._rfbCredentials
.password
=== undefined) {
1505 this.dispatchEvent(new CustomEvent(
1506 "credentialsrequired",
1507 { detail
: { types
: ["username", "password"] } }));
1511 this._sock
.send([0, 0, 0, this._rfbCredentials
.username
.length
]);
1512 this._sock
.send([0, 0, 0, this._rfbCredentials
.password
.length
]);
1513 this._sock
.sendString(this._rfbCredentials
.username
);
1514 this._sock
.sendString(this._rfbCredentials
.password
);
1515 this._rfbInitState
= "SecurityResult";
1519 _negotiateTightTunnels(numTunnels
) {
1520 const clientSupportedTunnelTypes
= {
1521 0: { vendor
: 'TGHT', signature
: 'NOTUNNEL' }
1523 const serverSupportedTunnelTypes
= {};
1524 // receive tunnel capabilities
1525 for (let i
= 0; i
< numTunnels
; i
++) {
1526 const capCode
= this._sock
.rQshift32();
1527 const capVendor
= this._sock
.rQshiftStr(4);
1528 const capSignature
= this._sock
.rQshiftStr(8);
1529 serverSupportedTunnelTypes
[capCode
] = { vendor
: capVendor
, signature
: capSignature
};
1532 Log
.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes
);
1534 // Siemens touch panels have a VNC server that supports NOTUNNEL,
1535 // but forgets to advertise it. Try to detect such servers by
1536 // looking for their custom tunnel type.
1537 if (serverSupportedTunnelTypes
[1] &&
1538 (serverSupportedTunnelTypes
[1].vendor
=== "SICR") &&
1539 (serverSupportedTunnelTypes
[1].signature
=== "SCHANNEL")) {
1540 Log
.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
1541 serverSupportedTunnelTypes
[0] = { vendor
: 'TGHT', signature
: 'NOTUNNEL' };
1544 // choose the notunnel type
1545 if (serverSupportedTunnelTypes
[0]) {
1546 if (serverSupportedTunnelTypes
[0].vendor
!= clientSupportedTunnelTypes
[0].vendor
||
1547 serverSupportedTunnelTypes
[0].signature
!= clientSupportedTunnelTypes
[0].signature
) {
1548 return this._fail("Client's tunnel type had the incorrect " +
1549 "vendor or signature");
1551 Log
.Debug("Selected tunnel type: " + clientSupportedTunnelTypes
[0]);
1552 this._sock
.send([0, 0, 0, 0]); // use NOTUNNEL
1553 return false; // wait until we receive the sub auth count to continue
1555 return this._fail("Server wanted tunnels, but doesn't support " +
1556 "the notunnel type");
1560 _negotiateTightAuth() {
1561 if (!this._rfbTightVNC
) { // first pass, do the tunnel negotiation
1562 if (this._sock
.rQwait("num tunnels", 4)) { return false; }
1563 const numTunnels
= this._sock
.rQshift32();
1564 if (numTunnels
> 0 && this._sock
.rQwait("tunnel capabilities", 16 * numTunnels
, 4)) { return false; }
1566 this._rfbTightVNC
= true;
1568 if (numTunnels
> 0) {
1569 this._negotiateTightTunnels(numTunnels
);
1570 return false; // wait until we receive the sub auth to continue
1574 // second pass, do the sub-auth negotiation
1575 if (this._sock
.rQwait("sub auth count", 4)) { return false; }
1576 const subAuthCount
= this._sock
.rQshift32();
1577 if (subAuthCount
=== 0) { // empty sub-auth list received means 'no auth' subtype selected
1578 this._rfbInitState
= 'SecurityResult';
1582 if (this._sock
.rQwait("sub auth capabilities", 16 * subAuthCount
, 4)) { return false; }
1584 const clientSupportedTypes
= {
1590 const serverSupportedTypes
= [];
1592 for (let i
= 0; i
< subAuthCount
; i
++) {
1593 this._sock
.rQshift32(); // capNum
1594 const capabilities
= this._sock
.rQshiftStr(12);
1595 serverSupportedTypes
.push(capabilities
);
1598 Log
.Debug("Server Tight authentication types: " + serverSupportedTypes
);
1600 for (let authType
in clientSupportedTypes
) {
1601 if (serverSupportedTypes
.indexOf(authType
) != -1) {
1602 this._sock
.send([0, 0, 0, clientSupportedTypes
[authType
]]);
1603 Log
.Debug("Selected authentication type: " + authType
);
1606 case 'STDVNOAUTH__': // no auth
1607 this._rfbInitState
= 'SecurityResult';
1609 case 'STDVVNCAUTH_': // VNC auth
1610 this._rfbAuthScheme
= 2;
1611 return this._initMsg();
1612 case 'TGHTULGNAUTH': // UNIX auth
1613 this._rfbAuthScheme
= 129;
1614 return this._initMsg();
1616 return this._fail("Unsupported tiny auth scheme " +
1617 "(scheme: " + authType
+ ")");
1622 return this._fail("No supported sub-auth types!");
1625 _negotiateAuthentication() {
1626 switch (this._rfbAuthScheme
) {
1628 if (this._rfbVersion
>= 3.8) {
1629 this._rfbInitState
= 'SecurityResult';
1632 this._rfbInitState
= 'ClientInitialisation';
1633 return this._initMsg();
1635 case 22: // XVP auth
1636 return this._negotiateXvpAuth();
1638 case 2: // VNC authentication
1639 return this._negotiateStdVNCAuth();
1641 case 16: // TightVNC Security Type
1642 return this._negotiateTightAuth();
1644 case 19: // VeNCrypt Security Type
1645 return this._negotiateVeNCryptAuth();
1647 case 129: // TightVNC UNIX Security Type
1648 return this._negotiateTightUnixAuth();
1651 return this._fail("Unsupported auth scheme (scheme: " +
1652 this._rfbAuthScheme
+ ")");
1656 _handleSecurityResult() {
1657 if (this._sock
.rQwait('VNC auth response ', 4)) { return false; }
1659 const status
= this._sock
.rQshift32();
1661 if (status
=== 0) { // OK
1662 this._rfbInitState
= 'ClientInitialisation';
1663 Log
.Debug('Authentication OK');
1664 return this._initMsg();
1666 if (this._rfbVersion
>= 3.8) {
1667 this._rfbInitState
= "SecurityReason";
1668 this._securityContext
= "security result";
1669 this._securityStatus
= status
;
1670 return this._initMsg();
1672 this.dispatchEvent(new CustomEvent(
1674 { detail
: { status
: status
} }));
1676 return this._fail("Security handshake failed");
1681 _negotiateServerInit() {
1682 if (this._sock
.rQwait("server initialization", 24)) { return false; }
1685 const width
= this._sock
.rQshift16();
1686 const height
= this._sock
.rQshift16();
1689 const bpp
= this._sock
.rQshift8();
1690 const depth
= this._sock
.rQshift8();
1691 const bigEndian
= this._sock
.rQshift8();
1692 const trueColor
= this._sock
.rQshift8();
1694 const redMax
= this._sock
.rQshift16();
1695 const greenMax
= this._sock
.rQshift16();
1696 const blueMax
= this._sock
.rQshift16();
1697 const redShift
= this._sock
.rQshift8();
1698 const greenShift
= this._sock
.rQshift8();
1699 const blueShift
= this._sock
.rQshift8();
1700 this._sock
.rQskipBytes(3); // padding
1702 // NB(directxman12): we don't want to call any callbacks or print messages until
1703 // *after* we're past the point where we could backtrack
1705 /* Connection name/title */
1706 const nameLength = this._sock.rQshift32();
1707 if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
1708 let name = this._sock.rQshiftStr(nameLength);
1709 name = decodeUTF8(name, true);
1711 if (this._rfbTightVNC) {
1712 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
1713 // In TightVNC mode, ServerInit message is extended
1714 const numServerMessages = this._sock.rQshift16();
1715 const numClientMessages = this._sock.rQshift16();
1716 const numEncodings = this._sock.rQshift16();
1717 this._sock.rQskipBytes(2); // padding
1719 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
1720 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
1722 // we don't actually do anything with the capability information that TIGHT sends,
1723 // so we just skip the all of this.
1725 // TIGHT server message capabilities
1726 this._sock.rQskipBytes(16 * numServerMessages);
1728 // TIGHT client message capabilities
1729 this._sock.rQskipBytes(16 * numClientMessages);
1731 // TIGHT encoding capabilities
1732 this._sock.rQskipBytes(16 * numEncodings);
1735 // NB(directxman12): these are down here so that we don't run them multiple times
1737 Log.Info("Screen: " + width + "x" + height +
1738 ", bpp: " + bpp + ", depth: " + depth +
1739 ", bigEndian: " + bigEndian +
1740 ", trueColor: " + trueColor +
1741 ", redMax: " + redMax +
1742 ", greenMax: " + greenMax +
1743 ", blueMax: " + blueMax +
1744 ", redShift: " + redShift +
1745 ", greenShift: " + greenShift +
1746 ", blueShift: " + blueShift);
1748 // we're past the point where we could backtrack, so it's safe to call this
1749 this._setDesktopName(name);
1750 this._resize(width, height);
1752 if (!this._viewOnly) { this._keyboard.grab(); }
1756 if (this._fbName === "Intel(r) AMT KVM") {
1757 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
1761 RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
1762 this._sendEncodings();
1763 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
1765 this._updateConnectionState('connected');
1772 // In preference order
1773 encs.push(encodings.encodingCopyRect);
1774 // Only supported with full depth support
1775 if (this._fbDepth == 24) {
1776 encs.push(encodings.encodingTight);
1777 encs.push(encodings.encodingTightPNG);
1778 encs.push(encodings.encodingHextile);
1779 encs.push(encodings.encodingRRE);
1781 encs.push(encodings.encodingRaw);
1783 // Psuedo-encoding settings
1784 encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
1785 encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
1787 encs.push(encodings.pseudoEncodingDesktopSize);
1788 encs.push(encodings.pseudoEncodingLastRect);
1789 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
1790 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
1791 encs.push(encodings.pseudoEncodingXvp);
1792 encs.push(encodings.pseudoEncodingFence);
1793 encs.push(encodings.pseudoEncodingContinuousUpdates);
1794 encs.push(encodings.pseudoEncodingDesktopName);
1795 encs.push(encodings.pseudoEncodingExtendedClipboard);
1797 if (this._fbDepth == 24) {
1798 encs.push(encodings.pseudoEncodingVMwareCursor);
1799 encs.push(encodings.pseudoEncodingCursor);
1802 RFB.messages.clientEncodings(this._sock, encs);
1805 /* RFB protocol initialization states:
1810 * ClientInitialization - not triggered by server message
1811 * ServerInitialization
1814 switch (this._rfbInitState
) {
1815 case 'ProtocolVersion':
1816 return this._negotiateProtocolVersion();
1819 return this._negotiateSecurity();
1821 case 'Authentication':
1822 return this._negotiateAuthentication();
1824 case 'SecurityResult':
1825 return this._handleSecurityResult();
1827 case 'SecurityReason':
1828 return this._handleSecurityReason();
1830 case 'ClientInitialisation':
1831 this._sock
.send([this._shared
? 1 : 0]); // ClientInitialisation
1832 this._rfbInitState
= 'ServerInitialisation';
1835 case 'ServerInitialisation':
1836 return this._negotiateServerInit();
1839 return this._fail("Unknown init state (state: " +
1840 this._rfbInitState
+ ")");
1844 _handleSetColourMapMsg() {
1845 Log
.Debug("SetColorMapEntries");
1847 return this._fail("Unexpected SetColorMapEntries message");
1850 _handleServerCutText() {
1851 Log
.Debug("ServerCutText");
1853 if (this._sock
.rQwait("ServerCutText header", 7, 1)) { return false; }
1855 this._sock
.rQskipBytes(3); // Padding
1857 let length
= this._sock
.rQshift32();
1858 length
= toSigned32bit(length
);
1860 if (this._sock
.rQwait("ServerCutText content", Math
.abs(length
), 8)) { return false; }
1864 const text
= this._sock
.rQshiftStr(length
);
1865 if (this._viewOnly
) {
1869 this.dispatchEvent(new CustomEvent(
1871 { detail
: { text
: text
} }));
1875 length
= Math
.abs(length
);
1876 const flags
= this._sock
.rQshift32();
1877 let formats
= flags
& 0x0000FFFF;
1878 let actions
= flags
& 0xFF000000;
1880 let isCaps
= (!!(actions
& extendedClipboardActionCaps
));
1882 this._clipboardServerCapabilitiesFormats
= {};
1883 this._clipboardServerCapabilitiesActions
= {};
1885 // Update our server capabilities for Formats
1886 for (let i
= 0; i
<= 15; i
++) {
1889 // Check if format flag is set.
1890 if ((formats
& index
)) {
1891 this._clipboardServerCapabilitiesFormats
[index
] = true;
1892 // We don't send unsolicited clipboard, so we
1894 this._sock
.rQshift32();
1898 // Update our server capabilities for Actions
1899 for (let i
= 24; i
<= 31; i
++) {
1901 this._clipboardServerCapabilitiesActions
[index
] = !!(actions
& index
);
1904 /* Caps handling done, send caps with the clients
1905 capabilities set as a response */
1906 let clientActions
= [
1907 extendedClipboardActionCaps
,
1908 extendedClipboardActionRequest
,
1909 extendedClipboardActionPeek
,
1910 extendedClipboardActionNotify
,
1911 extendedClipboardActionProvide
1913 RFB
.messages
.extendedClipboardCaps(this._sock
, clientActions
, {extendedClipboardFormatText
: 0});
1915 } else if (actions
=== extendedClipboardActionRequest
) {
1916 if (this._viewOnly
) {
1920 // Check if server has told us it can handle Provide and there is clipboard data to send.
1921 if (this._clipboardText
!= null &&
1922 this._clipboardServerCapabilitiesActions
[extendedClipboardActionProvide
]) {
1924 if (formats
& extendedClipboardFormatText
) {
1925 RFB
.messages
.extendedClipboardProvide(this._sock
, [extendedClipboardFormatText
], [this._clipboardText
]);
1929 } else if (actions
=== extendedClipboardActionPeek
) {
1930 if (this._viewOnly
) {
1934 if (this._clipboardServerCapabilitiesActions
[extendedClipboardActionNotify
]) {
1936 if (this._clipboardText
!= null) {
1937 RFB
.messages
.extendedClipboardNotify(this._sock
, [extendedClipboardFormatText
]);
1939 RFB
.messages
.extendedClipboardNotify(this._sock
, []);
1943 } else if (actions
=== extendedClipboardActionNotify
) {
1944 if (this._viewOnly
) {
1948 if (this._clipboardServerCapabilitiesActions
[extendedClipboardActionRequest
]) {
1950 if (formats
& extendedClipboardFormatText
) {
1951 RFB
.messages
.extendedClipboardRequest(this._sock
, [extendedClipboardFormatText
]);
1955 } else if (actions
=== extendedClipboardActionProvide
) {
1956 if (this._viewOnly
) {
1960 if (!(formats
& extendedClipboardFormatText
)) {
1963 // Ignore what we had in our clipboard client side.
1964 this._clipboardText
= null;
1966 // FIXME: Should probably verify that this data was actually requested
1967 let zlibStream
= this._sock
.rQshiftBytes(length
- 4);
1968 let streamInflator
= new Inflator();
1969 let textData
= null;
1971 streamInflator
.setInput(zlibStream
);
1972 for (let i
= 0; i
<= 15; i
++) {
1973 let format
= 1 << i
;
1975 if (formats
& format
) {
1978 let sizeArray
= streamInflator
.inflate(4);
1980 size
|= (sizeArray
[0] << 24);
1981 size
|= (sizeArray
[1] << 16);
1982 size
|= (sizeArray
[2] << 8);
1983 size
|= (sizeArray
[3]);
1984 let chunk
= streamInflator
.inflate(size
);
1986 if (format
=== extendedClipboardFormatText
) {
1991 streamInflator
.setInput(null);
1993 if (textData
!== null) {
1995 for (let i
= 0; i
< textData
.length
; i
++) {
1996 tmpText
+= String
.fromCharCode(textData
[i
]);
2000 textData
= decodeUTF8(textData
);
2001 if ((textData
.length
> 0) && "\0" === textData
.charAt(textData
.length
- 1)) {
2002 textData
= textData
.slice(0, -1);
2005 textData
= textData
.replace("\r\n", "\n");
2007 this.dispatchEvent(new CustomEvent(
2009 { detail
: { text
: textData
} }));
2012 return this._fail("Unexpected action in extended clipboard message: " + actions
);
2018 _handleServerFenceMsg() {
2019 if (this._sock
.rQwait("ServerFence header", 8, 1)) { return false; }
2020 this._sock
.rQskipBytes(3); // Padding
2021 let flags
= this._sock
.rQshift32();
2022 let length
= this._sock
.rQshift8();
2024 if (this._sock
.rQwait("ServerFence payload", length
, 9)) { return false; }
2027 Log
.Warn("Bad payload length (" + length
+ ") in fence response");
2031 const payload
= this._sock
.rQshiftStr(length
);
2033 this._supportsFence
= true;
2038 * (1<<0) - BlockBefore
2039 * (1<<1) - BlockAfter
2044 if (!(flags
& (1<<31))) {
2045 return this._fail("Unexpected fence response");
2048 // Filter out unsupported flags
2049 // FIXME: support syncNext
2050 flags
&= (1<<0) | (1<<1);
2052 // BlockBefore and BlockAfter are automatically handled by
2053 // the fact that we process each incoming message
2055 RFB
.messages
.clientFence(this._sock
, flags
, payload
);
2061 if (this._sock
.rQwait("XVP version and message", 3, 1)) { return false; }
2062 this._sock
.rQskipBytes(1); // Padding
2063 const xvpVer
= this._sock
.rQshift8();
2064 const xvpMsg
= this._sock
.rQshift8();
2068 Log
.Error("XVP Operation Failed");
2071 this._rfbXvpVer
= xvpVer
;
2072 Log
.Info("XVP extensions enabled (version " + this._rfbXvpVer
+ ")");
2073 this._setCapability("power", true);
2076 this._fail("Illegal server XVP message (msg: " + xvpMsg
+ ")");
2085 if (this._FBU
.rects
> 0) {
2088 msgType
= this._sock
.rQshift8();
2093 case 0: // FramebufferUpdate
2094 ret
= this._framebufferUpdate();
2095 if (ret
&& !this._enabledContinuousUpdates
) {
2096 RFB
.messages
.fbUpdateRequest(this._sock
, true, 0, 0,
2097 this._fbWidth
, this._fbHeight
);
2101 case 1: // SetColorMapEntries
2102 return this._handleSetColourMapMsg();
2106 this.dispatchEvent(new CustomEvent(
2111 case 3: // ServerCutText
2112 return this._handleServerCutText();
2114 case 150: // EndOfContinuousUpdates
2115 first
= !this._supportsContinuousUpdates
;
2116 this._supportsContinuousUpdates
= true;
2117 this._enabledContinuousUpdates
= false;
2119 this._enabledContinuousUpdates
= true;
2120 this._updateContinuousUpdates();
2121 Log
.Info("Enabling continuous updates.");
2123 // FIXME: We need to send a framebufferupdaterequest here
2124 // if we add support for turning off continuous updates
2128 case 248: // ServerFence
2129 return this._handleServerFenceMsg();
2132 return this._handleXvpMsg();
2135 this._fail("Unexpected server message (type " + msgType
+ ")");
2136 Log
.Debug("sock.rQslice(0, 30): " + this._sock
.rQslice(0, 30));
2142 this._flushing
= false;
2143 // Resume processing
2144 if (this._sock
.rQlen
> 0) {
2145 this._handleMessage();
2149 _framebufferUpdate() {
2150 if (this._FBU
.rects
=== 0) {
2151 if (this._sock
.rQwait("FBU header", 3, 1)) { return false; }
2152 this._sock
.rQskipBytes(1); // Padding
2153 this._FBU
.rects
= this._sock
.rQshift16();
2155 // Make sure the previous frame is fully rendered first
2156 // to avoid building up an excessive queue
2157 if (this._display
.pending()) {
2158 this._flushing
= true;
2159 this._display
.flush();
2164 while (this._FBU
.rects
> 0) {
2165 if (this._FBU
.encoding
=== null) {
2166 if (this._sock
.rQwait("rect header", 12)) { return false; }
2167 /* New FramebufferUpdate */
2169 const hdr
= this._sock
.rQshiftBytes(12);
2170 this._FBU
.x
= (hdr
[0] << 8) + hdr
[1];
2171 this._FBU
.y
= (hdr
[2] << 8) + hdr
[3];
2172 this._FBU
.width
= (hdr
[4] << 8) + hdr
[5];
2173 this._FBU
.height
= (hdr
[6] << 8) + hdr
[7];
2174 this._FBU
.encoding
= parseInt((hdr
[8] << 24) + (hdr
[9] << 16) +
2175 (hdr
[10] << 8) + hdr
[11], 10);
2178 if (!this._handleRect()) {
2183 this._FBU
.encoding
= null;
2186 this._display
.flip();
2188 return true; // We finished this FBU
2192 switch (this._FBU
.encoding
) {
2193 case encodings
.pseudoEncodingLastRect
:
2194 this._FBU
.rects
= 1; // Will be decreased when we return
2197 case encodings
.pseudoEncodingVMwareCursor
:
2198 return this._handleVMwareCursor();
2200 case encodings
.pseudoEncodingCursor
:
2201 return this._handleCursor();
2203 case encodings
.pseudoEncodingQEMUExtendedKeyEvent
:
2204 this._qemuExtKeyEventSupported
= true;
2207 case encodings
.pseudoEncodingDesktopName
:
2208 return this._handleDesktopName();
2210 case encodings
.pseudoEncodingDesktopSize
:
2211 this._resize(this._FBU
.width
, this._FBU
.height
);
2214 case encodings
.pseudoEncodingExtendedDesktopSize
:
2215 return this._handleExtendedDesktopSize();
2218 return this._handleDataRect();
2222 _handleVMwareCursor() {
2223 const hotx
= this._FBU
.x
; // hotspot-x
2224 const hoty
= this._FBU
.y
; // hotspot-y
2225 const w
= this._FBU
.width
;
2226 const h
= this._FBU
.height
;
2227 if (this._sock
.rQwait("VMware cursor encoding", 1)) {
2231 const cursorType
= this._sock
.rQshift8();
2233 this._sock
.rQshift8(); //Padding
2236 const bytesPerPixel
= 4;
2239 if (cursorType
== 0) {
2240 //Used to filter away unimportant bits.
2241 //OR is used for correct conversion in js.
2242 const PIXEL_MASK
= 0xffffff00 | 0;
2243 rgba
= new Array(w
* h
* bytesPerPixel
);
2245 if (this._sock
.rQwait("VMware cursor classic encoding",
2246 (w
* h
* bytesPerPixel
) * 2, 2)) {
2250 let andMask
= new Array(w
* h
);
2251 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2252 andMask
[pixel
] = this._sock
.rQshift32();
2255 let xorMask
= new Array(w
* h
);
2256 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2257 xorMask
[pixel
] = this._sock
.rQshift32();
2260 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2261 if (andMask
[pixel
] == 0) {
2262 //Fully opaque pixel
2263 let bgr
= xorMask
[pixel
];
2264 let r
= bgr
>> 8 & 0xff;
2265 let g
= bgr
>> 16 & 0xff;
2266 let b
= bgr
>> 24 & 0xff;
2268 rgba
[(pixel
* bytesPerPixel
) ] = r
; //r
2269 rgba
[(pixel
* bytesPerPixel
) + 1 ] = g
; //g
2270 rgba
[(pixel
* bytesPerPixel
) + 2 ] = b
; //b
2271 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff; //a
2273 } else if ((andMask
[pixel
] & PIXEL_MASK
) ==
2275 //Only screen value matters, no mouse colouring
2276 if (xorMask
[pixel
] == 0) {
2278 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2279 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2280 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2281 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0x00;
2283 } else if ((xorMask
[pixel
] & PIXEL_MASK
) ==
2285 //Inverted pixel, not supported in browsers.
2286 //Fully opaque instead.
2287 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2288 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2289 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2290 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2294 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2295 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2296 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2297 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2302 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2303 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2304 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2305 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2310 } else if (cursorType
== 1) {
2311 if (this._sock
.rQwait("VMware cursor alpha encoding",
2316 rgba
= new Array(w
* h
* bytesPerPixel
);
2318 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2319 let data
= this._sock
.rQshift32();
2321 rgba
[(pixel
* 4) ] = data
>> 24 & 0xff; //r
2322 rgba
[(pixel
* 4) + 1 ] = data
>> 16 & 0xff; //g
2323 rgba
[(pixel
* 4) + 2 ] = data
>> 8 & 0xff; //b
2324 rgba
[(pixel
* 4) + 3 ] = data
& 0xff; //a
2328 Log
.Warn("The given cursor type is not supported: "
2329 + cursorType
+ " given.");
2333 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
2339 const hotx
= this._FBU
.x
; // hotspot-x
2340 const hoty
= this._FBU
.y
; // hotspot-y
2341 const w
= this._FBU
.width
;
2342 const h
= this._FBU
.height
;
2344 const pixelslength
= w
* h
* 4;
2345 const masklength
= Math
.ceil(w
/ 8) * h
;
2347 let bytes
= pixelslength
+ masklength
;
2348 if (this._sock
.rQwait("cursor encoding", bytes
)) {
2352 // Decode from BGRX pixels + bit mask to RGBA
2353 const pixels
= this._sock
.rQshiftBytes(pixelslength
);
2354 const mask
= this._sock
.rQshiftBytes(masklength
);
2355 let rgba
= new Uint8Array(w
* h
* 4);
2358 for (let y
= 0; y
< h
; y
++) {
2359 for (let x
= 0; x
< w
; x
++) {
2360 let maskIdx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/ 8);
2361 let alpha
= (mask
[maskIdx
] << (x
% 8)) & 0x80 ? 255 : 0;
2362 rgba
[pixIdx
] = pixels
[pixIdx
+ 2];
2363 rgba
[pixIdx
+ 1] = pixels
[pixIdx
+ 1];
2364 rgba
[pixIdx
+ 2] = pixels
[pixIdx
];
2365 rgba
[pixIdx
+ 3] = alpha
;
2370 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
2375 _handleDesktopName() {
2376 if (this._sock
.rQwait("DesktopName", 4)) {
2380 let length
= this._sock
.rQshift32();
2382 if (this._sock
.rQwait("DesktopName", length
, 4)) {
2386 let name
= this._sock
.rQshiftStr(length
);
2387 name
= decodeUTF8(name
, true);
2389 this._setDesktopName(name
);
2394 _handleExtendedDesktopSize() {
2395 if (this._sock
.rQwait("ExtendedDesktopSize", 4)) {
2399 const numberOfScreens
= this._sock
.rQpeek8();
2401 let bytes
= 4 + (numberOfScreens
* 16);
2402 if (this._sock
.rQwait("ExtendedDesktopSize", bytes
)) {
2406 const firstUpdate
= !this._supportsSetDesktopSize
;
2407 this._supportsSetDesktopSize
= true;
2409 // Normally we only apply the current resize mode after a
2410 // window resize event. However there is no such trigger on the
2411 // initial connect. And we don't know if the server supports
2412 // resizing until we've gotten here.
2414 this._requestRemoteResize();
2417 this._sock
.rQskipBytes(1); // number-of-screens
2418 this._sock
.rQskipBytes(3); // padding
2420 for (let i
= 0; i
< numberOfScreens
; i
+= 1) {
2421 // Save the id and flags of the first screen
2423 this._screenID
= this._sock
.rQshiftBytes(4); // id
2424 this._sock
.rQskipBytes(2); // x-position
2425 this._sock
.rQskipBytes(2); // y-position
2426 this._sock
.rQskipBytes(2); // width
2427 this._sock
.rQskipBytes(2); // height
2428 this._screenFlags
= this._sock
.rQshiftBytes(4); // flags
2430 this._sock
.rQskipBytes(16);
2435 * The x-position indicates the reason for the change:
2437 * 0 - server resized on its own
2438 * 1 - this client requested the resize
2439 * 2 - another client requested the resize
2442 // We need to handle errors when we requested the resize.
2443 if (this._FBU
.x
=== 1 && this._FBU
.y
!== 0) {
2445 // The y-position indicates the status code from the server
2446 switch (this._FBU
.y
) {
2448 msg
= "Resize is administratively prohibited";
2451 msg
= "Out of resources";
2454 msg
= "Invalid screen layout";
2457 msg
= "Unknown reason";
2460 Log
.Warn("Server did not accept the resize request: "
2463 this._resize(this._FBU
.width
, this._FBU
.height
);
2470 let decoder
= this._decoders
[this._FBU
.encoding
];
2472 this._fail("Unsupported encoding (encoding: " +
2473 this._FBU
.encoding
+ ")");
2478 return decoder
.decodeRect(this._FBU
.x
, this._FBU
.y
,
2479 this._FBU
.width
, this._FBU
.height
,
2480 this._sock
, this._display
,
2483 this._fail("Error decoding rect: " + err
);
2488 _updateContinuousUpdates() {
2489 if (!this._enabledContinuousUpdates
) { return; }
2491 RFB
.messages
.enableContinuousUpdates(this._sock
, true, 0, 0,
2492 this._fbWidth
, this._fbHeight
);
2495 _resize(width
, height
) {
2496 this._fbWidth
= width
;
2497 this._fbHeight
= height
;
2499 this._display
.resize(this._fbWidth
, this._fbHeight
);
2501 // Adjust the visible viewport based on the new dimensions
2503 this._updateScale();
2505 this._updateContinuousUpdates();
2509 if (this._rfbXvpVer
< ver
) { return; }
2510 Log
.Info("Sending XVP operation " + op
+ " (version " + ver
+ ")");
2511 RFB
.messages
.xvpOp(this._sock
, ver
, op
);
2514 _updateCursor(rgba
, hotx
, hoty
, w
, h
) {
2515 this._cursorImage
= {
2517 hotx
: hotx
, hoty
: hoty
, w
: w
, h
: h
,
2519 this._refreshCursor();
2522 _shouldShowDotCursor() {
2523 // Called when this._cursorImage is updated
2524 if (!this._showDotCursor
) {
2525 // User does not want to see the dot, so...
2529 // The dot should not be shown if the cursor is already visible,
2530 // i.e. contains at least one not-fully-transparent pixel.
2531 // So iterate through all alpha bytes in rgba and stop at the
2533 for (let i
= 3; i
< this._cursorImage
.rgbaPixels
.length
; i
+= 4) {
2534 if (this._cursorImage
.rgbaPixels
[i
]) {
2539 // At this point, we know that the cursor is fully transparent, and
2540 // the user wants to see the dot instead of this.
2545 if (this._rfbConnectionState
!== "connecting" &&
2546 this._rfbConnectionState
!== "connected") {
2549 const image
= this._shouldShowDotCursor() ? RFB
.cursors
.dot
: this._cursorImage
;
2550 this._cursor
.change(image
.rgbaPixels
,
2551 image
.hotx
, image
.hoty
,
2556 static genDES(password
, challenge
) {
2557 const passwordChars
= password
.split('').map(c
=> c
.charCodeAt(0));
2558 return (new DES(passwordChars
)).encrypt(challenge
);
2564 keyEvent(sock
, keysym
, down
) {
2565 const buff
= sock
._sQ
;
2566 const offset
= sock
._sQlen
;
2568 buff
[offset
] = 4; // msg-type
2569 buff
[offset
+ 1] = down
;
2571 buff
[offset
+ 2] = 0;
2572 buff
[offset
+ 3] = 0;
2574 buff
[offset
+ 4] = (keysym
>> 24);
2575 buff
[offset
+ 5] = (keysym
>> 16);
2576 buff
[offset
+ 6] = (keysym
>> 8);
2577 buff
[offset
+ 7] = keysym
;
2583 QEMUExtendedKeyEvent(sock
, keysym
, down
, keycode
) {
2584 function getRFBkeycode(xtScanCode
) {
2585 const upperByte
= (keycode
>> 8);
2586 const lowerByte
= (keycode
& 0x00ff);
2587 if (upperByte
=== 0xe0 && lowerByte
< 0x7f) {
2588 return lowerByte
| 0x80;
2593 const buff
= sock
._sQ
;
2594 const offset
= sock
._sQlen
;
2596 buff
[offset
] = 255; // msg-type
2597 buff
[offset
+ 1] = 0; // sub msg-type
2599 buff
[offset
+ 2] = (down
>> 8);
2600 buff
[offset
+ 3] = down
;
2602 buff
[offset
+ 4] = (keysym
>> 24);
2603 buff
[offset
+ 5] = (keysym
>> 16);
2604 buff
[offset
+ 6] = (keysym
>> 8);
2605 buff
[offset
+ 7] = keysym
;
2607 const RFBkeycode
= getRFBkeycode(keycode
);
2609 buff
[offset
+ 8] = (RFBkeycode
>> 24);
2610 buff
[offset
+ 9] = (RFBkeycode
>> 16);
2611 buff
[offset
+ 10] = (RFBkeycode
>> 8);
2612 buff
[offset
+ 11] = RFBkeycode
;
2618 pointerEvent(sock
, x
, y
, mask
) {
2619 const buff
= sock
._sQ
;
2620 const offset
= sock
._sQlen
;
2622 buff
[offset
] = 5; // msg-type
2624 buff
[offset
+ 1] = mask
;
2626 buff
[offset
+ 2] = x
>> 8;
2627 buff
[offset
+ 3] = x
;
2629 buff
[offset
+ 4] = y
>> 8;
2630 buff
[offset
+ 5] = y
;
2636 // Used to build Notify and Request data.
2637 _buildExtendedClipboardFlags(actions
, formats
) {
2638 let data
= new Uint8Array(4);
2639 let formatFlag
= 0x00000000;
2640 let actionFlag
= 0x00000000;
2642 for (let i
= 0; i
< actions
.length
; i
++) {
2643 actionFlag
|= actions
[i
];
2646 for (let i
= 0; i
< formats
.length
; i
++) {
2647 formatFlag
|= formats
[i
];
2650 data
[0] = actionFlag
>> 24; // Actions
2651 data
[1] = 0x00; // Reserved
2652 data
[2] = 0x00; // Reserved
2653 data
[3] = formatFlag
; // Formats
2658 extendedClipboardProvide(sock
, formats
, inData
) {
2659 // Deflate incomming data and their sizes
2660 let deflator
= new Deflator();
2661 let dataToDeflate
= [];
2663 for (let i
= 0; i
< formats
.length
; i
++) {
2664 // We only support the format Text at this time
2665 if (formats
[i
] != extendedClipboardFormatText
) {
2666 throw new Error("Unsupported extended clipboard format for Provide message.");
2669 // Change lone \r or \n into \r\n as defined in rfbproto
2670 inData
[i
] = inData
[i
].replace(/\r\n|\r|\n/gm, "\r\n");
2672 // Check if it already has \0
2673 let text
= encodeUTF8(inData
[i
] + "\0");
2675 dataToDeflate
.push( (text
.length
>> 24) & 0xFF,
2676 (text
.length
>> 16) & 0xFF,
2677 (text
.length
>> 8) & 0xFF,
2678 (text
.length
& 0xFF));
2680 for (let j
= 0; j
< text
.length
; j
++) {
2681 dataToDeflate
.push(text
.charCodeAt(j
));
2685 let deflatedData
= deflator
.deflate(new Uint8Array(dataToDeflate
));
2687 // Build data to send
2688 let data
= new Uint8Array(4 + deflatedData
.length
);
2689 data
.set(RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionProvide
],
2691 data
.set(deflatedData
, 4);
2693 RFB
.messages
.clientCutText(sock
, data
, true);
2696 extendedClipboardNotify(sock
, formats
) {
2697 let flags
= RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionNotify
],
2699 RFB
.messages
.clientCutText(sock
, flags
, true);
2702 extendedClipboardRequest(sock
, formats
) {
2703 let flags
= RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionRequest
],
2705 RFB
.messages
.clientCutText(sock
, flags
, true);
2708 extendedClipboardCaps(sock
, actions
, formats
) {
2709 let formatKeys
= Object
.keys(formats
);
2710 let data
= new Uint8Array(4 + (4 * formatKeys
.length
));
2712 formatKeys
.map(x
=> parseInt(x
));
2713 formatKeys
.sort((a
, b
) => a
- b
);
2715 data
.set(RFB
.messages
._buildExtendedClipboardFlags(actions
, []));
2718 for (let i
= 0; i
< formatKeys
.length
; i
++) {
2719 data
[loopOffset
] = formats
[formatKeys
[i
]] >> 24;
2720 data
[loopOffset
+ 1] = formats
[formatKeys
[i
]] >> 16;
2721 data
[loopOffset
+ 2] = formats
[formatKeys
[i
]] >> 8;
2722 data
[loopOffset
+ 3] = formats
[formatKeys
[i
]] >> 0;
2725 data
[3] |= (1 << formatKeys
[i
]); // Update our format flags
2728 RFB
.messages
.clientCutText(sock
, data
, true);
2731 clientCutText(sock
, data
, extended
= false) {
2732 const buff
= sock
._sQ
;
2733 const offset
= sock
._sQlen
;
2735 buff
[offset
] = 6; // msg-type
2737 buff
[offset
+ 1] = 0; // padding
2738 buff
[offset
+ 2] = 0; // padding
2739 buff
[offset
+ 3] = 0; // padding
2743 length
= toUnsigned32bit(-data
.length
);
2745 length
= data
.length
;
2748 buff
[offset
+ 4] = length
>> 24;
2749 buff
[offset
+ 5] = length
>> 16;
2750 buff
[offset
+ 6] = length
>> 8;
2751 buff
[offset
+ 7] = length
;
2755 // We have to keep track of from where in the data we begin creating the
2756 // buffer for the flush in the next iteration.
2759 let remaining
= data
.length
;
2760 while (remaining
> 0) {
2762 let flushSize
= Math
.min(remaining
, (sock
._sQbufferSize
- sock
._sQlen
));
2763 for (let i
= 0; i
< flushSize
; i
++) {
2764 buff
[sock
._sQlen
+ i
] = data
[dataOffset
+ i
];
2767 sock
._sQlen
+= flushSize
;
2770 remaining
-= flushSize
;
2771 dataOffset
+= flushSize
;
2776 setDesktopSize(sock
, width
, height
, id
, flags
) {
2777 const buff
= sock
._sQ
;
2778 const offset
= sock
._sQlen
;
2780 buff
[offset
] = 251; // msg-type
2781 buff
[offset
+ 1] = 0; // padding
2782 buff
[offset
+ 2] = width
>> 8; // width
2783 buff
[offset
+ 3] = width
;
2784 buff
[offset
+ 4] = height
>> 8; // height
2785 buff
[offset
+ 5] = height
;
2787 buff
[offset
+ 6] = 1; // number-of-screens
2788 buff
[offset
+ 7] = 0; // padding
2791 buff
[offset
+ 8] = id
>> 24; // id
2792 buff
[offset
+ 9] = id
>> 16;
2793 buff
[offset
+ 10] = id
>> 8;
2794 buff
[offset
+ 11] = id
;
2795 buff
[offset
+ 12] = 0; // x-position
2796 buff
[offset
+ 13] = 0;
2797 buff
[offset
+ 14] = 0; // y-position
2798 buff
[offset
+ 15] = 0;
2799 buff
[offset
+ 16] = width
>> 8; // width
2800 buff
[offset
+ 17] = width
;
2801 buff
[offset
+ 18] = height
>> 8; // height
2802 buff
[offset
+ 19] = height
;
2803 buff
[offset
+ 20] = flags
>> 24; // flags
2804 buff
[offset
+ 21] = flags
>> 16;
2805 buff
[offset
+ 22] = flags
>> 8;
2806 buff
[offset
+ 23] = flags
;
2812 clientFence(sock
, flags
, payload
) {
2813 const buff
= sock
._sQ
;
2814 const offset
= sock
._sQlen
;
2816 buff
[offset
] = 248; // msg-type
2818 buff
[offset
+ 1] = 0; // padding
2819 buff
[offset
+ 2] = 0; // padding
2820 buff
[offset
+ 3] = 0; // padding
2822 buff
[offset
+ 4] = flags
>> 24; // flags
2823 buff
[offset
+ 5] = flags
>> 16;
2824 buff
[offset
+ 6] = flags
>> 8;
2825 buff
[offset
+ 7] = flags
;
2827 const n
= payload
.length
;
2829 buff
[offset
+ 8] = n
; // length
2831 for (let i
= 0; i
< n
; i
++) {
2832 buff
[offset
+ 9 + i
] = payload
.charCodeAt(i
);
2835 sock
._sQlen
+= 9 + n
;
2839 enableContinuousUpdates(sock
, enable
, x
, y
, width
, height
) {
2840 const buff
= sock
._sQ
;
2841 const offset
= sock
._sQlen
;
2843 buff
[offset
] = 150; // msg-type
2844 buff
[offset
+ 1] = enable
; // enable-flag
2846 buff
[offset
+ 2] = x
>> 8; // x
2847 buff
[offset
+ 3] = x
;
2848 buff
[offset
+ 4] = y
>> 8; // y
2849 buff
[offset
+ 5] = y
;
2850 buff
[offset
+ 6] = width
>> 8; // width
2851 buff
[offset
+ 7] = width
;
2852 buff
[offset
+ 8] = height
>> 8; // height
2853 buff
[offset
+ 9] = height
;
2859 pixelFormat(sock
, depth
, trueColor
) {
2860 const buff
= sock
._sQ
;
2861 const offset
= sock
._sQlen
;
2867 } else if (depth
> 8) {
2873 const bits
= Math
.floor(depth
/3);
2875 buff
[offset
] = 0; // msg-type
2877 buff
[offset
+ 1] = 0; // padding
2878 buff
[offset
+ 2] = 0; // padding
2879 buff
[offset
+ 3] = 0; // padding
2881 buff
[offset
+ 4] = bpp
; // bits-per-pixel
2882 buff
[offset
+ 5] = depth
; // depth
2883 buff
[offset
+ 6] = 0; // little-endian
2884 buff
[offset
+ 7] = trueColor
? 1 : 0; // true-color
2886 buff
[offset
+ 8] = 0; // red-max
2887 buff
[offset
+ 9] = (1 << bits
) - 1; // red-max
2889 buff
[offset
+ 10] = 0; // green-max
2890 buff
[offset
+ 11] = (1 << bits
) - 1; // green-max
2892 buff
[offset
+ 12] = 0; // blue-max
2893 buff
[offset
+ 13] = (1 << bits
) - 1; // blue-max
2895 buff
[offset
+ 14] = bits
* 0; // red-shift
2896 buff
[offset
+ 15] = bits
* 1; // green-shift
2897 buff
[offset
+ 16] = bits
* 2; // blue-shift
2899 buff
[offset
+ 17] = 0; // padding
2900 buff
[offset
+ 18] = 0; // padding
2901 buff
[offset
+ 19] = 0; // padding
2907 clientEncodings(sock
, encodings
) {
2908 const buff
= sock
._sQ
;
2909 const offset
= sock
._sQlen
;
2911 buff
[offset
] = 2; // msg-type
2912 buff
[offset
+ 1] = 0; // padding
2914 buff
[offset
+ 2] = encodings
.length
>> 8;
2915 buff
[offset
+ 3] = encodings
.length
;
2918 for (let i
= 0; i
< encodings
.length
; i
++) {
2919 const enc
= encodings
[i
];
2920 buff
[j
] = enc
>> 24;
2921 buff
[j
+ 1] = enc
>> 16;
2922 buff
[j
+ 2] = enc
>> 8;
2928 sock
._sQlen
+= j
- offset
;
2932 fbUpdateRequest(sock
, incremental
, x
, y
, w
, h
) {
2933 const buff
= sock
._sQ
;
2934 const offset
= sock
._sQlen
;
2936 if (typeof(x
) === "undefined") { x
= 0; }
2937 if (typeof(y
) === "undefined") { y
= 0; }
2939 buff
[offset
] = 3; // msg-type
2940 buff
[offset
+ 1] = incremental
? 1 : 0;
2942 buff
[offset
+ 2] = (x
>> 8) & 0xFF;
2943 buff
[offset
+ 3] = x
& 0xFF;
2945 buff
[offset
+ 4] = (y
>> 8) & 0xFF;
2946 buff
[offset
+ 5] = y
& 0xFF;
2948 buff
[offset
+ 6] = (w
>> 8) & 0xFF;
2949 buff
[offset
+ 7] = w
& 0xFF;
2951 buff
[offset
+ 8] = (h
>> 8) & 0xFF;
2952 buff
[offset
+ 9] = h
& 0xFF;
2958 xvpOp(sock
, ver
, op
) {
2959 const buff
= sock
._sQ
;
2960 const offset
= sock
._sQlen
;
2962 buff
[offset
] = 250; // msg-type
2963 buff
[offset
+ 1] = 0; // padding
2965 buff
[offset
+ 2] = ver
;
2966 buff
[offset
+ 3] = op
;
2975 rgbaPixels
: new Uint8Array(),
2981 /* eslint-disable indent */
2982 rgbaPixels
: new Uint8Array([
2983 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2984 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
2985 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2987 /* eslint-enable indent */