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";
28 import "./util/polyfill.js";
30 import RawDecoder
from "./decoders/raw.js";
31 import CopyRectDecoder
from "./decoders/copyrect.js";
32 import RREDecoder
from "./decoders/rre.js";
33 import HextileDecoder
from "./decoders/hextile.js";
34 import TightDecoder
from "./decoders/tight.js";
35 import TightPNGDecoder
from "./decoders/tightpng.js";
37 // How many seconds to wait for a disconnect to finish
38 const DISCONNECT_TIMEOUT
= 3;
39 const DEFAULT_BACKGROUND
= 'rgb(40, 40, 40)';
41 // Minimum wait (ms) between two mouse moves
42 const MOUSE_MOVE_DELAY
= 17;
45 const WHEEL_STEP
= 50; // Pixels needed for one step
46 const WHEEL_LINE_HEIGHT
= 19; // Assumed pixels for one line step
49 const GESTURE_ZOOMSENS
= 75;
50 const GESTURE_SCRLSENS
= 50;
51 const DOUBLE_TAP_TIMEOUT
= 1000;
52 const DOUBLE_TAP_THRESHOLD
= 50;
54 // Extended clipboard pseudo-encoding formats
55 const extendedClipboardFormatText
= 1;
56 /*eslint-disable no-unused-vars */
57 const extendedClipboardFormatRtf
= 1 << 1;
58 const extendedClipboardFormatHtml
= 1 << 2;
59 const extendedClipboardFormatDib
= 1 << 3;
60 const extendedClipboardFormatFiles
= 1 << 4;
63 // Extended clipboard pseudo-encoding actions
64 const extendedClipboardActionCaps
= 1 << 24;
65 const extendedClipboardActionRequest
= 1 << 25;
66 const extendedClipboardActionPeek
= 1 << 26;
67 const extendedClipboardActionNotify
= 1 << 27;
68 const extendedClipboardActionProvide
= 1 << 28;
71 export default class RFB
extends EventTargetMixin
{
72 constructor(target
, url
, options
) {
74 throw new Error("Must specify target");
77 throw new Error("Must specify URL");
82 this._target
= target
;
86 options
= options
|| {};
87 this._rfbCredentials
= options
.credentials
|| {};
88 this._shared
= 'shared' in options
? !!options
.shared
: true;
89 this._repeaterID
= options
.repeaterID
|| '';
90 this._wsProtocols
= options
.wsProtocols
|| [];
93 this._rfbConnectionState
= '';
94 this._rfbInitState
= '';
95 this._rfbAuthScheme
= -1;
96 this._rfbCleanDisconnect
= true;
98 // Server capabilities
100 this._rfbMaxVersion
= 3.8;
101 this._rfbTightVNC
= false;
102 this._rfbVeNCryptState
= 0;
110 this._capabilities
= { power
: false };
112 this._supportsFence
= false;
114 this._supportsContinuousUpdates
= false;
115 this._enabledContinuousUpdates
= false;
117 this._supportsSetDesktopSize
= false;
119 this._screenFlags
= 0;
121 this._qemuExtKeyEventSupported
= false;
123 this._clipboardText
= null;
124 this._clipboardServerCapabilitiesActions
= {};
125 this._clipboardServerCapabilitiesFormats
= {};
128 this._sock
= null; // Websock object
129 this._display
= null; // Display object
130 this._flushing
= false; // Display flushing state
131 this._keyboard
= null; // Keyboard input handler object
132 this._gestures
= null; // Gesture input handler object
135 this._disconnTimer
= null; // disconnection timer
136 this._resizeTimeout
= null; // resize rate limiting
137 this._mouseMoveTimer
= null;
153 this._mouseButtonMask
= 0;
154 this._mouseLastMoveTime
= 0;
155 this._viewportDragging
= false;
156 this._viewportDragPos
= {};
157 this._viewportHasMoved
= false;
158 this._accumulatedWheelDeltaX
= 0;
159 this._accumulatedWheelDeltaY
= 0;
162 this._gestureLastTapTime
= null;
163 this._gestureFirstDoubleTapEv
= null;
164 this._gestureLastMagnitudeX
= 0;
165 this._gestureLastMagnitudeY
= 0;
167 // Bound event handlers
168 this._eventHandlers
= {
169 focusCanvas
: this._focusCanvas
.bind(this),
170 windowResize
: this._windowResize
.bind(this),
171 handleMouse
: this._handleMouse
.bind(this),
172 handleWheel
: this._handleWheel
.bind(this),
173 handleGesture
: this._handleGesture
.bind(this),
177 Log
.Debug(">> RFB.constructor");
179 // Create DOM elements
180 this._screen
= document
.createElement('div');
181 this._screen
.style
.display
= 'flex';
182 this._screen
.style
.width
= '100%';
183 this._screen
.style
.height
= '100%';
184 this._screen
.style
.overflow
= 'auto';
185 this._screen
.style
.background
= DEFAULT_BACKGROUND
;
186 this._canvas
= document
.createElement('canvas');
187 this._canvas
.style
.margin
= 'auto';
188 // Some browsers add an outline on focus
189 this._canvas
.style
.outline
= 'none';
190 // IE miscalculates width without this :(
191 this._canvas
.style
.flexShrink
= '0';
192 this._canvas
.width
= 0;
193 this._canvas
.height
= 0;
194 this._canvas
.tabIndex
= -1;
195 this._screen
.appendChild(this._canvas
);
198 this._cursor
= new Cursor();
200 // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
201 // it. Result: no cursor at all until a window border or an edit field
202 // is hit blindly. But there are also VNC servers that draw the cursor
203 // in the framebuffer and don't send the empty local cursor. There is
204 // no way to satisfy both sides.
206 // The spec is unclear on this "initial cursor" issue. Many other
207 // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
208 // initial cursor instead.
209 this._cursorImage
= RFB
.cursors
.none
;
211 // populate decoder array with objects
212 this._decoders
[encodings
.encodingRaw
] = new RawDecoder();
213 this._decoders
[encodings
.encodingCopyRect
] = new CopyRectDecoder();
214 this._decoders
[encodings
.encodingRRE
] = new RREDecoder();
215 this._decoders
[encodings
.encodingHextile
] = new HextileDecoder();
216 this._decoders
[encodings
.encodingTight
] = new TightDecoder();
217 this._decoders
[encodings
.encodingTightPNG
] = new TightPNGDecoder();
219 // NB: nothing that needs explicit teardown should be done
220 // before this point, since this can throw an exception
222 this._display
= new Display(this._canvas
);
224 Log
.Error("Display exception: " + exc
);
227 this._display
.onflush
= this._onFlush
.bind(this);
229 this._keyboard
= new Keyboard(this._canvas
);
230 this._keyboard
.onkeyevent
= this._handleKeyEvent
.bind(this);
232 this._gestures
= new GestureHandler();
234 this._sock
= new Websock();
235 this._sock
.on('message', () => {
236 this._handleMessage();
238 this._sock
.on('open', () => {
239 if ((this._rfbConnectionState
=== 'connecting') &&
240 (this._rfbInitState
=== '')) {
241 this._rfbInitState
= 'ProtocolVersion';
242 Log
.Debug("Starting VNC handshake");
244 this._fail("Unexpected server connection while " +
245 this._rfbConnectionState
);
248 this._sock
.on('close', (e
) => {
249 Log
.Debug("WebSocket on-close event");
252 msg
= "(code: " + e
.code
;
254 msg
+= ", reason: " + e
.reason
;
258 switch (this._rfbConnectionState
) {
260 this._fail("Connection closed " + msg
);
263 // Handle disconnects that were initiated server-side
264 this._updateConnectionState('disconnecting');
265 this._updateConnectionState('disconnected');
267 case 'disconnecting':
268 // Normal disconnection path
269 this._updateConnectionState('disconnected');
272 this._fail("Unexpected server disconnect " +
273 "when already disconnected " + msg
);
276 this._fail("Unexpected server disconnect before connecting " +
280 this._sock
.off('close');
282 this._sock
.on('error', e
=> Log
.Warn("WebSocket on-error event"));
284 // Slight delay of the actual connection so that the caller has
285 // time to set up callbacks
286 setTimeout(this._updateConnectionState
.bind(this, 'connecting'));
288 Log
.Debug("<< RFB.constructor");
290 // ===== PROPERTIES =====
292 this.dragViewport
= false;
293 this.focusOnClick
= true;
295 this._viewOnly
= false;
296 this._clipViewport
= false;
297 this._scaleViewport
= false;
298 this._resizeSession
= false;
300 this._showDotCursor
= false;
301 if (options
.showDotCursor
!== undefined) {
302 Log
.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated");
303 this._showDotCursor
= options
.showDotCursor
;
306 this._qualityLevel
= 6;
307 this._compressionLevel
= 2;
310 // ===== PROPERTIES =====
312 get viewOnly() { return this._viewOnly
; }
313 set viewOnly(viewOnly
) {
314 this._viewOnly
= viewOnly
;
316 if (this._rfbConnectionState
=== "connecting" ||
317 this._rfbConnectionState
=== "connected") {
319 this._keyboard
.ungrab();
321 this._keyboard
.grab();
326 get capabilities() { return this._capabilities
; }
328 get touchButton() { return 0; }
329 set touchButton(button
) { Log
.Warn("Using old API!"); }
331 get clipViewport() { return this._clipViewport
; }
332 set clipViewport(viewport
) {
333 this._clipViewport
= viewport
;
337 get scaleViewport() { return this._scaleViewport
; }
338 set scaleViewport(scale
) {
339 this._scaleViewport
= scale
;
340 // Scaling trumps clipping, so we may need to adjust
341 // clipping when enabling or disabling scaling
342 if (scale
&& this._clipViewport
) {
346 if (!scale
&& this._clipViewport
) {
351 get resizeSession() { return this._resizeSession
; }
352 set resizeSession(resize
) {
353 this._resizeSession
= resize
;
355 this._requestRemoteResize();
359 get showDotCursor() { return this._showDotCursor
; }
360 set showDotCursor(show
) {
361 this._showDotCursor
= show
;
362 this._refreshCursor();
365 get background() { return this._screen
.style
.background
; }
366 set background(cssValue
) { this._screen
.style
.background
= cssValue
; }
369 return this._qualityLevel
;
371 set qualityLevel(qualityLevel
) {
372 if (!Number
.isInteger(qualityLevel
) || qualityLevel
< 0 || qualityLevel
> 9) {
373 Log
.Error("qualityLevel must be an integer between 0 and 9");
377 if (this._qualityLevel
=== qualityLevel
) {
381 this._qualityLevel
= qualityLevel
;
383 if (this._rfbConnectionState
=== 'connected') {
384 this._sendEncodings();
388 get compressionLevel() {
389 return this._compressionLevel
;
391 set compressionLevel(compressionLevel
) {
392 if (!Number
.isInteger(compressionLevel
) || compressionLevel
< 0 || compressionLevel
> 9) {
393 Log
.Error("compressionLevel must be an integer between 0 and 9");
397 if (this._compressionLevel
=== compressionLevel
) {
401 this._compressionLevel
= compressionLevel
;
403 if (this._rfbConnectionState
=== 'connected') {
404 this._sendEncodings();
408 // ===== PUBLIC METHODS =====
411 this._updateConnectionState('disconnecting');
412 this._sock
.off('error');
413 this._sock
.off('message');
414 this._sock
.off('open');
417 sendCredentials(creds
) {
418 this._rfbCredentials
= creds
;
419 setTimeout(this._initMsg
.bind(this), 0);
423 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
424 Log
.Info("Sending Ctrl-Alt-Del");
426 this.sendKey(KeyTable
.XK_Control_L
, "ControlLeft", true);
427 this.sendKey(KeyTable
.XK_Alt_L
, "AltLeft", true);
428 this.sendKey(KeyTable
.XK_Delete
, "Delete", true);
429 this.sendKey(KeyTable
.XK_Delete
, "Delete", false);
430 this.sendKey(KeyTable
.XK_Alt_L
, "AltLeft", false);
431 this.sendKey(KeyTable
.XK_Control_L
, "ControlLeft", false);
446 // Send a key press. If 'down' is not specified then send a down key
447 // followed by an up key.
448 sendKey(keysym
, code
, down
) {
449 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
451 if (down
=== undefined) {
452 this.sendKey(keysym
, code
, true);
453 this.sendKey(keysym
, code
, false);
457 const scancode
= XtScancode
[code
];
459 if (this._qemuExtKeyEventSupported
&& scancode
) {
461 keysym
= keysym
|| 0;
463 Log
.Info("Sending key (" + (down
? "down" : "up") + "): keysym " + keysym
+ ", scancode " + scancode
);
465 RFB
.messages
.QEMUExtendedKeyEvent(this._sock
, keysym
, down
, scancode
);
470 Log
.Info("Sending keysym (" + (down
? "down" : "up") + "): " + keysym
);
471 RFB
.messages
.keyEvent(this._sock
, keysym
, down
? 1 : 0);
476 this._canvas
.focus();
483 clipboardPasteFrom(text
) {
484 if (this._rfbConnectionState
!== 'connected' || this._viewOnly
) { return; }
486 if (this._clipboardServerCapabilitiesFormats
[extendedClipboardFormatText
] &&
487 this._clipboardServerCapabilitiesActions
[extendedClipboardActionNotify
]) {
489 this._clipboardText
= text
;
490 RFB
.messages
.extendedClipboardNotify(this._sock
, [extendedClipboardFormatText
]);
492 let data
= new Uint8Array(text
.length
);
493 for (let i
= 0; i
< text
.length
; i
++) {
494 // FIXME: text can have values outside of Latin1/Uint8
495 data
[i
] = text
.charCodeAt(i
);
498 RFB
.messages
.clientCutText(this._sock
, data
);
502 // ===== PRIVATE METHODS =====
505 Log
.Debug(">> RFB.connect");
507 Log
.Info("connecting to " + this._url
);
510 // WebSocket.onopen transitions to the RFB init states
511 this._sock
.open(this._url
, this._wsProtocols
);
513 if (e
.name
=== 'SyntaxError') {
514 this._fail("Invalid host or port (" + e
+ ")");
516 this._fail("Error when opening socket (" + e
+ ")");
520 // Make our elements part of the page
521 this._target
.appendChild(this._screen
);
523 this._gestures
.attach(this._canvas
);
525 this._cursor
.attach(this._canvas
);
526 this._refreshCursor();
528 // Monitor size changes of the screen
529 // FIXME: Use ResizeObserver, or hidden overflow
530 window
.addEventListener('resize', this._eventHandlers
.windowResize
);
532 // Always grab focus on some kind of click event
533 this._canvas
.addEventListener("mousedown", this._eventHandlers
.focusCanvas
);
534 this._canvas
.addEventListener("touchstart", this._eventHandlers
.focusCanvas
);
537 this._canvas
.addEventListener('mousedown', this._eventHandlers
.handleMouse
);
538 this._canvas
.addEventListener('mouseup', this._eventHandlers
.handleMouse
);
539 this._canvas
.addEventListener('mousemove', this._eventHandlers
.handleMouse
);
540 // Prevent middle-click pasting (see handler for why we bind to document)
541 this._canvas
.addEventListener('click', this._eventHandlers
.handleMouse
);
542 // preventDefault() on mousedown doesn't stop this event for some
543 // reason so we have to explicitly block it
544 this._canvas
.addEventListener('contextmenu', this._eventHandlers
.handleMouse
);
547 this._canvas
.addEventListener("wheel", this._eventHandlers
.handleWheel
);
550 this._canvas
.addEventListener("gesturestart", this._eventHandlers
.handleGesture
);
551 this._canvas
.addEventListener("gesturemove", this._eventHandlers
.handleGesture
);
552 this._canvas
.addEventListener("gestureend", this._eventHandlers
.handleGesture
);
554 Log
.Debug("<< RFB.connect");
558 Log
.Debug(">> RFB.disconnect");
559 this._cursor
.detach();
560 this._canvas
.removeEventListener("gesturestart", this._eventHandlers
.handleGesture
);
561 this._canvas
.removeEventListener("gesturemove", this._eventHandlers
.handleGesture
);
562 this._canvas
.removeEventListener("gestureend", this._eventHandlers
.handleGesture
);
563 this._canvas
.removeEventListener("wheel", this._eventHandlers
.handleWheel
);
564 this._canvas
.removeEventListener('mousedown', this._eventHandlers
.handleMouse
);
565 this._canvas
.removeEventListener('mouseup', this._eventHandlers
.handleMouse
);
566 this._canvas
.removeEventListener('mousemove', this._eventHandlers
.handleMouse
);
567 this._canvas
.removeEventListener('click', this._eventHandlers
.handleMouse
);
568 this._canvas
.removeEventListener('contextmenu', this._eventHandlers
.handleMouse
);
569 this._canvas
.removeEventListener("mousedown", this._eventHandlers
.focusCanvas
);
570 this._canvas
.removeEventListener("touchstart", this._eventHandlers
.focusCanvas
);
571 window
.removeEventListener('resize', this._eventHandlers
.windowResize
);
572 this._keyboard
.ungrab();
573 this._gestures
.detach();
576 this._target
.removeChild(this._screen
);
578 if (e
.name
=== 'NotFoundError') {
579 // Some cases where the initial connection fails
580 // can disconnect before the _screen is created
585 clearTimeout(this._resizeTimeout
);
586 clearTimeout(this._mouseMoveTimer
);
587 Log
.Debug("<< RFB.disconnect");
590 _focusCanvas(event
) {
591 // Respect earlier handlers' request to not do side-effects
592 if (event
.defaultPrevented
) {
596 if (!this.focusOnClick
) {
603 _setDesktopName(name
) {
605 this.dispatchEvent(new CustomEvent(
607 { detail
: { name
: this._fbName
} }));
610 _windowResize(event
) {
611 // If the window resized then our screen element might have
612 // as well. Update the viewport dimensions.
613 window
.requestAnimationFrame(() => {
618 if (this._resizeSession
) {
619 // Request changing the resolution of the remote display to
620 // the size of the local browser viewport.
622 // In order to not send multiple requests before the browser-resize
623 // is finished we wait 0.5 seconds before sending the request.
624 clearTimeout(this._resizeTimeout
);
625 this._resizeTimeout
= setTimeout(this._requestRemoteResize
.bind(this), 500);
629 // Update state of clipping in Display object, and make sure the
630 // configured viewport matches the current screen size
632 const curClip
= this._display
.clipViewport
;
633 let newClip
= this._clipViewport
;
635 if (this._scaleViewport
) {
636 // Disable viewport clipping if we are scaling
640 if (curClip
!== newClip
) {
641 this._display
.clipViewport
= newClip
;
645 // When clipping is enabled, the screen is limited to
646 // the size of the container.
647 const size
= this._screenSize();
648 this._display
.viewportChangeSize(size
.w
, size
.h
);
649 this._fixScrollbars();
654 if (!this._scaleViewport
) {
655 this._display
.scale
= 1.0;
657 const size
= this._screenSize();
658 this._display
.autoscale(size
.w
, size
.h
);
660 this._fixScrollbars();
663 // Requests a change of remote desktop size. This message is an extension
664 // and may only be sent if we have received an ExtendedDesktopSize message
665 _requestRemoteResize() {
666 clearTimeout(this._resizeTimeout
);
667 this._resizeTimeout
= null;
669 if (!this._resizeSession
|| this._viewOnly
||
670 !this._supportsSetDesktopSize
) {
674 const size
= this._screenSize();
675 RFB
.messages
.setDesktopSize(this._sock
,
676 Math
.floor(size
.w
), Math
.floor(size
.h
),
677 this._screenID
, this._screenFlags
);
679 Log
.Debug('Requested new desktop size: ' +
680 size
.w
+ 'x' + size
.h
);
683 // Gets the the size of the available screen
685 let r
= this._screen
.getBoundingClientRect();
686 return { w
: r
.width
, h
: r
.height
};
690 // This is a hack because Chrome screws up the calculation
691 // for when scrollbars are needed. So to fix it we temporarily
692 // toggle them off and on.
693 const orig
= this._screen
.style
.overflow
;
694 this._screen
.style
.overflow
= 'hidden';
695 // Force Chrome to recalculate the layout by asking for
696 // an element's dimensions
697 this._screen
.getBoundingClientRect();
698 this._screen
.style
.overflow
= orig
;
706 * disconnected - permanent state
708 _updateConnectionState(state
) {
709 const oldstate
= this._rfbConnectionState
;
711 if (state
=== oldstate
) {
712 Log
.Debug("Already in state '" + state
+ "', ignoring");
716 // The 'disconnected' state is permanent for each RFB object
717 if (oldstate
=== 'disconnected') {
718 Log
.Error("Tried changing state of a disconnected RFB object");
722 // Ensure proper transitions before doing anything
725 if (oldstate
!== 'connecting') {
726 Log
.Error("Bad transition to connected state, " +
727 "previous connection state: " + oldstate
);
733 if (oldstate
!== 'disconnecting') {
734 Log
.Error("Bad transition to disconnected state, " +
735 "previous connection state: " + oldstate
);
741 if (oldstate
!== '') {
742 Log
.Error("Bad transition to connecting state, " +
743 "previous connection state: " + oldstate
);
748 case 'disconnecting':
749 if (oldstate
!== 'connected' && oldstate
!== 'connecting') {
750 Log
.Error("Bad transition to disconnecting state, " +
751 "previous connection state: " + oldstate
);
757 Log
.Error("Unknown connection state: " + state
);
761 // State change actions
763 this._rfbConnectionState
= state
;
765 Log
.Debug("New state '" + state
+ "', was '" + oldstate
+ "'.");
767 if (this._disconnTimer
&& state
!== 'disconnecting') {
768 Log
.Debug("Clearing disconnect timer");
769 clearTimeout(this._disconnTimer
);
770 this._disconnTimer
= null;
772 // make sure we don't get a double event
773 this._sock
.off('close');
782 this.dispatchEvent(new CustomEvent("connect", { detail
: {} }));
785 case 'disconnecting':
788 this._disconnTimer
= setTimeout(() => {
789 Log
.Error("Disconnection timed out.");
790 this._updateConnectionState('disconnected');
791 }, DISCONNECT_TIMEOUT
* 1000);
795 this.dispatchEvent(new CustomEvent(
796 "disconnect", { detail
:
797 { clean
: this._rfbCleanDisconnect
} }));
802 /* Print errors and disconnect
804 * The parameter 'details' is used for information that
805 * should be logged but not sent to the user interface.
808 switch (this._rfbConnectionState
) {
809 case 'disconnecting':
810 Log
.Error("Failed when disconnecting: " + details
);
813 Log
.Error("Failed while connected: " + details
);
816 Log
.Error("Failed when connecting: " + details
);
819 Log
.Error("RFB failure: " + details
);
822 this._rfbCleanDisconnect
= false; //This is sent to the UI
824 // Transition to disconnected without waiting for socket to close
825 this._updateConnectionState('disconnecting');
826 this._updateConnectionState('disconnected');
831 _setCapability(cap
, val
) {
832 this._capabilities
[cap
] = val
;
833 this.dispatchEvent(new CustomEvent("capabilities",
834 { detail
: { capabilities
: this._capabilities
} }));
838 if (this._sock
.rQlen
=== 0) {
839 Log
.Warn("handleMessage called on an empty receive queue");
843 switch (this._rfbConnectionState
) {
845 Log
.Error("Got data while disconnected");
849 if (this._flushing
) {
852 if (!this._normalMsg()) {
855 if (this._sock
.rQlen
=== 0) {
866 _handleKeyEvent(keysym
, code
, down
) {
867 this.sendKey(keysym
, code
, down
);
872 * We don't check connection status or viewOnly here as the
873 * mouse events might be used to control the viewport
876 if (ev
.type
=== 'click') {
878 * Note: This is only needed for the 'click' event as it fails
879 * to fire properly for the target element so we have
880 * to listen on the document element instead.
882 if (ev
.target
!== this._canvas
) {
887 // FIXME: if we're in view-only and not dragging,
888 // should we stop events?
889 ev
.stopPropagation();
892 if ((ev
.type
=== 'click') || (ev
.type
=== 'contextmenu')) {
896 let pos
= clientToElement(ev
.clientX
, ev
.clientY
,
901 setCapture(this._canvas
);
902 this._handleMouseButton(pos
.x
, pos
.y
,
903 true, 1 << ev
.button
);
906 this._handleMouseButton(pos
.x
, pos
.y
,
907 false, 1 << ev
.button
);
910 this._handleMouseMove(pos
.x
, pos
.y
);
915 _handleMouseButton(x
, y
, down
, bmask
) {
916 if (this.dragViewport
) {
917 if (down
&& !this._viewportDragging
) {
918 this._viewportDragging
= true;
919 this._viewportDragPos
= {'x': x
, 'y': y
};
920 this._viewportHasMoved
= false;
922 // Skip sending mouse events
925 this._viewportDragging
= false;
927 // If we actually performed a drag then we are done
928 // here and should not send any mouse events
929 if (this._viewportHasMoved
) {
933 // Otherwise we treat this as a mouse click event.
934 // Send the button down event here, as the button up
935 // event is sent at the end of this function.
936 this._sendMouse(x
, y
, bmask
);
940 // Flush waiting move event first
941 if (this._mouseMoveTimer
!== null) {
942 clearTimeout(this._mouseMoveTimer
);
943 this._mouseMoveTimer
= null;
944 this._sendMouse(x
, y
, this._mouseButtonMask
);
948 this._mouseButtonMask
|= bmask
;
950 this._mouseButtonMask
&= ~bmask
;
953 this._sendMouse(x
, y
, this._mouseButtonMask
);
956 _handleMouseMove(x
, y
) {
957 if (this._viewportDragging
) {
958 const deltaX
= this._viewportDragPos
.x
- x
;
959 const deltaY
= this._viewportDragPos
.y
- y
;
961 if (this._viewportHasMoved
|| (Math
.abs(deltaX
) > dragThreshold
||
962 Math
.abs(deltaY
) > dragThreshold
)) {
963 this._viewportHasMoved
= true;
965 this._viewportDragPos
= {'x': x
, 'y': y
};
966 this._display
.viewportChangePos(deltaX
, deltaY
);
969 // Skip sending mouse events
973 this._mousePos
= { 'x': x
, 'y': y
};
975 // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms
976 if (this._mouseMoveTimer
== null) {
978 const timeSinceLastMove
= Date
.now() - this._mouseLastMoveTime
;
979 if (timeSinceLastMove
> MOUSE_MOVE_DELAY
) {
980 this._sendMouse(x
, y
, this._mouseButtonMask
);
981 this._mouseLastMoveTime
= Date
.now();
983 // Too soon since the latest move, wait the remaining time
984 this._mouseMoveTimer
= setTimeout(() => {
985 this._handleDelayedMouseMove();
986 }, MOUSE_MOVE_DELAY
- timeSinceLastMove
);
991 _handleDelayedMouseMove() {
992 this._mouseMoveTimer
= null;
993 this._sendMouse(this._mousePos
.x
, this._mousePos
.y
,
994 this._mouseButtonMask
);
995 this._mouseLastMoveTime
= Date
.now();
998 _sendMouse(x
, y
, mask
) {
999 if (this._rfbConnectionState
!== 'connected') { return; }
1000 if (this._viewOnly
) { return; } // View only, skip mouse events
1002 RFB
.messages
.pointerEvent(this._sock
, this._display
.absX(x
),
1003 this._display
.absY(y
), mask
);
1007 if (this._rfbConnectionState
!== 'connected') { return; }
1008 if (this._viewOnly
) { return; } // View only, skip mouse events
1010 ev
.stopPropagation();
1011 ev
.preventDefault();
1013 let pos
= clientToElement(ev
.clientX
, ev
.clientY
,
1019 // Pixel units unless it's non-zero.
1020 // Note that if deltamode is line or page won't matter since we aren't
1021 // sending the mouse wheel delta to the server anyway.
1022 // The difference between pixel and line can be important however since
1023 // we have a threshold that can be smaller than the line height.
1024 if (ev
.deltaMode
!== 0) {
1025 dX
*= WHEEL_LINE_HEIGHT
;
1026 dY
*= WHEEL_LINE_HEIGHT
;
1029 // Mouse wheel events are sent in steps over VNC. This means that the VNC
1030 // protocol can't handle a wheel event with specific distance or speed.
1031 // Therefor, if we get a lot of small mouse wheel events we combine them.
1032 this._accumulatedWheelDeltaX
+= dX
;
1033 this._accumulatedWheelDeltaY
+= dY
;
1035 // Generate a mouse wheel step event when the accumulated delta
1036 // for one of the axes is large enough.
1037 if (Math
.abs(this._accumulatedWheelDeltaX
) >= WHEEL_STEP
) {
1038 if (this._accumulatedWheelDeltaX
< 0) {
1039 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 5);
1040 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 5);
1041 } else if (this._accumulatedWheelDeltaX
> 0) {
1042 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 6);
1043 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 6);
1046 this._accumulatedWheelDeltaX
= 0;
1048 if (Math
.abs(this._accumulatedWheelDeltaY
) >= WHEEL_STEP
) {
1049 if (this._accumulatedWheelDeltaY
< 0) {
1050 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 3);
1051 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 3);
1052 } else if (this._accumulatedWheelDeltaY
> 0) {
1053 this._handleMouseButton(pos
.x
, pos
.y
, true, 1 << 4);
1054 this._handleMouseButton(pos
.x
, pos
.y
, false, 1 << 4);
1057 this._accumulatedWheelDeltaY
= 0;
1061 _fakeMouseMove(ev
, elementX
, elementY
) {
1062 this._handleMouseMove(elementX
, elementY
);
1063 this._cursor
.move(ev
.detail
.clientX
, ev
.detail
.clientY
);
1066 _handleTapEvent(ev
, bmask
) {
1067 let pos
= clientToElement(ev
.detail
.clientX
, ev
.detail
.clientY
,
1070 // If the user quickly taps multiple times we assume they meant to
1071 // hit the same spot, so slightly adjust coordinates
1073 if ((this._gestureLastTapTime
!== null) &&
1074 ((Date
.now() - this._gestureLastTapTime
) < DOUBLE_TAP_TIMEOUT
) &&
1075 (this._gestureFirstDoubleTapEv
.detail
.type
=== ev
.detail
.type
)) {
1076 let dx
= this._gestureFirstDoubleTapEv
.detail
.clientX
- ev
.detail
.clientX
;
1077 let dy
= this._gestureFirstDoubleTapEv
.detail
.clientY
- ev
.detail
.clientY
;
1078 let distance
= Math
.hypot(dx
, dy
);
1080 if (distance
< DOUBLE_TAP_THRESHOLD
) {
1081 pos
= clientToElement(this._gestureFirstDoubleTapEv
.detail
.clientX
,
1082 this._gestureFirstDoubleTapEv
.detail
.clientY
,
1085 this._gestureFirstDoubleTapEv
= ev
;
1088 this._gestureFirstDoubleTapEv
= ev
;
1090 this._gestureLastTapTime
= Date
.now();
1092 this._fakeMouseMove(this._gestureFirstDoubleTapEv
, pos
.x
, pos
.y
);
1093 this._handleMouseButton(pos
.x
, pos
.y
, true, bmask
);
1094 this._handleMouseButton(pos
.x
, pos
.y
, false, bmask
);
1097 _handleGesture(ev
) {
1100 let pos
= clientToElement(ev
.detail
.clientX
, ev
.detail
.clientY
,
1103 case 'gesturestart':
1104 switch (ev
.detail
.type
) {
1106 this._handleTapEvent(ev
, 0x1);
1109 this._handleTapEvent(ev
, 0x4);
1112 this._handleTapEvent(ev
, 0x2);
1115 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1116 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x1);
1119 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1120 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x4);
1124 this._gestureLastMagnitudeX
= ev
.detail
.magnitudeX
;
1125 this._gestureLastMagnitudeY
= ev
.detail
.magnitudeY
;
1126 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1129 this._gestureLastMagnitudeX
= Math
.hypot(ev
.detail
.magnitudeX
,
1130 ev
.detail
.magnitudeY
);
1131 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1137 switch (ev
.detail
.type
) {
1144 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1147 // Always scroll in the same position.
1148 // We don't know if the mouse was moved so we need to move it
1150 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1151 while ((ev
.detail
.magnitudeY
- this._gestureLastMagnitudeY
) > GESTURE_SCRLSENS
) {
1152 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x8);
1153 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x8);
1154 this._gestureLastMagnitudeY
+= GESTURE_SCRLSENS
;
1156 while ((ev
.detail
.magnitudeY
- this._gestureLastMagnitudeY
) < -GESTURE_SCRLSENS
) {
1157 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x10);
1158 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x10);
1159 this._gestureLastMagnitudeY
-= GESTURE_SCRLSENS
;
1161 while ((ev
.detail
.magnitudeX
- this._gestureLastMagnitudeX
) > GESTURE_SCRLSENS
) {
1162 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x20);
1163 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x20);
1164 this._gestureLastMagnitudeX
+= GESTURE_SCRLSENS
;
1166 while ((ev
.detail
.magnitudeX
- this._gestureLastMagnitudeX
) < -GESTURE_SCRLSENS
) {
1167 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x40);
1168 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x40);
1169 this._gestureLastMagnitudeX
-= GESTURE_SCRLSENS
;
1173 // Always scroll in the same position.
1174 // We don't know if the mouse was moved so we need to move it
1176 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1177 magnitude
= Math
.hypot(ev
.detail
.magnitudeX
, ev
.detail
.magnitudeY
);
1178 if (Math
.abs(magnitude
- this._gestureLastMagnitudeX
) > GESTURE_ZOOMSENS
) {
1179 this._handleKeyEvent(KeyTable
.XK_Control_L
, "ControlLeft", true);
1180 while ((magnitude
- this._gestureLastMagnitudeX
) > GESTURE_ZOOMSENS
) {
1181 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x8);
1182 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x8);
1183 this._gestureLastMagnitudeX
+= GESTURE_ZOOMSENS
;
1185 while ((magnitude
- this._gestureLastMagnitudeX
) < -GESTURE_ZOOMSENS
) {
1186 this._handleMouseButton(pos
.x
, pos
.y
, true, 0x10);
1187 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x10);
1188 this._gestureLastMagnitudeX
-= GESTURE_ZOOMSENS
;
1191 this._handleKeyEvent(KeyTable
.XK_Control_L
, "ControlLeft", false);
1197 switch (ev
.detail
.type
) {
1205 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1206 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x1);
1209 this._fakeMouseMove(ev
, pos
.x
, pos
.y
);
1210 this._handleMouseButton(pos
.x
, pos
.y
, false, 0x4);
1219 _negotiateProtocolVersion() {
1220 if (this._sock
.rQwait("version", 12)) {
1224 const sversion
= this._sock
.rQshiftStr(12).substr(4, 7);
1225 Log
.Info("Server ProtocolVersion: " + sversion
);
1228 case "000.000": // UltraVNC repeater
1232 case "003.006": // UltraVNC
1233 case "003.889": // Apple Remote Desktop
1234 this._rfbVersion
= 3.3;
1237 this._rfbVersion
= 3.7;
1240 case "004.000": // Intel AMT KVM
1241 case "004.001": // RealVNC 4.6
1242 case "005.000": // RealVNC 5.3
1243 this._rfbVersion
= 3.8;
1246 return this._fail("Invalid server version " + sversion
);
1250 let repeaterID
= "ID:" + this._repeaterID
;
1251 while (repeaterID
.length
< 250) {
1254 this._sock
.sendString(repeaterID
);
1258 if (this._rfbVersion
> this._rfbMaxVersion
) {
1259 this._rfbVersion
= this._rfbMaxVersion
;
1262 const cversion
= "00" + parseInt(this._rfbVersion
, 10) +
1263 ".00" + ((this._rfbVersion
* 10) % 10);
1264 this._sock
.sendString("RFB " + cversion
+ "\n");
1265 Log
.Debug('Sent ProtocolVersion: ' + cversion
);
1267 this._rfbInitState
= 'Security';
1270 _negotiateSecurity() {
1271 // Polyfill since IE and PhantomJS doesn't have
1272 // TypedArray.includes()
1273 function includes(item
, array
) {
1274 for (let i
= 0; i
< array
.length
; i
++) {
1275 if (array
[i
] === item
) {
1282 if (this._rfbVersion
>= 3.7) {
1283 // Server sends supported list, client decides
1284 const numTypes
= this._sock
.rQshift8();
1285 if (this._sock
.rQwait("security type", numTypes
, 1)) { return false; }
1287 if (numTypes
=== 0) {
1288 this._rfbInitState
= "SecurityReason";
1289 this._securityContext
= "no security types";
1290 this._securityStatus
= 1;
1291 return this._initMsg();
1294 const types
= this._sock
.rQshiftBytes(numTypes
);
1295 Log
.Debug("Server security types: " + types
);
1297 // Look for each auth in preferred order
1298 if (includes(1, types
)) {
1299 this._rfbAuthScheme
= 1; // None
1300 } else if (includes(22, types
)) {
1301 this._rfbAuthScheme
= 22; // XVP
1302 } else if (includes(16, types
)) {
1303 this._rfbAuthScheme
= 16; // Tight
1304 } else if (includes(2, types
)) {
1305 this._rfbAuthScheme
= 2; // VNC Auth
1306 } else if (includes(19, types
)) {
1307 this._rfbAuthScheme
= 19; // VeNCrypt Auth
1309 return this._fail("Unsupported security types (types: " + types
+ ")");
1312 this._sock
.send([this._rfbAuthScheme
]);
1315 if (this._sock
.rQwait("security scheme", 4)) { return false; }
1316 this._rfbAuthScheme
= this._sock
.rQshift32();
1318 if (this._rfbAuthScheme
== 0) {
1319 this._rfbInitState
= "SecurityReason";
1320 this._securityContext
= "authentication scheme";
1321 this._securityStatus
= 1;
1322 return this._initMsg();
1326 this._rfbInitState
= 'Authentication';
1327 Log
.Debug('Authenticating using scheme: ' + this._rfbAuthScheme
);
1329 return this._initMsg(); // jump to authentication
1332 _handleSecurityReason() {
1333 if (this._sock
.rQwait("reason length", 4)) {
1336 const strlen
= this._sock
.rQshift32();
1340 if (this._sock
.rQwait("reason", strlen
, 4)) { return false; }
1341 reason
= this._sock
.rQshiftStr(strlen
);
1344 if (reason
!== "") {
1345 this.dispatchEvent(new CustomEvent(
1347 { detail
: { status
: this._securityStatus
,
1348 reason
: reason
} }));
1350 return this._fail("Security negotiation failed on " +
1351 this._securityContext
+
1352 " (reason: " + reason
+ ")");
1354 this.dispatchEvent(new CustomEvent(
1356 { detail
: { status
: this._securityStatus
} }));
1358 return this._fail("Security negotiation failed on " +
1359 this._securityContext
);
1364 _negotiateXvpAuth() {
1365 if (this._rfbCredentials
.username
=== undefined ||
1366 this._rfbCredentials
.password
=== undefined ||
1367 this._rfbCredentials
.target
=== undefined) {
1368 this.dispatchEvent(new CustomEvent(
1369 "credentialsrequired",
1370 { detail
: { types
: ["username", "password", "target"] } }));
1374 const xvpAuthStr
= String
.fromCharCode(this._rfbCredentials
.username
.length
) +
1375 String
.fromCharCode(this._rfbCredentials
.target
.length
) +
1376 this._rfbCredentials
.username
+
1377 this._rfbCredentials
.target
;
1378 this._sock
.sendString(xvpAuthStr
);
1379 this._rfbAuthScheme
= 2;
1380 return this._negotiateAuthentication();
1383 // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
1384 _negotiateVeNCryptAuth() {
1386 // waiting for VeNCrypt version
1387 if (this._rfbVeNCryptState
== 0) {
1388 if (this._sock
.rQwait("vencrypt version", 2)) { return false; }
1390 const major
= this._sock
.rQshift8();
1391 const minor
= this._sock
.rQshift8();
1393 if (!(major
== 0 && minor
== 2)) {
1394 return this._fail("Unsupported VeNCrypt version " + major
+ "." + minor
);
1397 this._sock
.send([0, 2]);
1398 this._rfbVeNCryptState
= 1;
1402 if (this._rfbVeNCryptState
== 1) {
1403 if (this._sock
.rQwait("vencrypt ack", 1)) { return false; }
1405 const res
= this._sock
.rQshift8();
1408 return this._fail("VeNCrypt failure " + res
);
1411 this._rfbVeNCryptState
= 2;
1413 // must fall through here (i.e. no "else if"), beacause we may have already received
1414 // the subtypes length and won't be called again
1416 if (this._rfbVeNCryptState
== 2) { // waiting for subtypes length
1417 if (this._sock
.rQwait("vencrypt subtypes length", 1)) { return false; }
1419 const subtypesLength
= this._sock
.rQshift8();
1420 if (subtypesLength
< 1) {
1421 return this._fail("VeNCrypt subtypes empty");
1424 this._rfbVeNCryptSubtypesLength
= subtypesLength
;
1425 this._rfbVeNCryptState
= 3;
1428 // waiting for subtypes list
1429 if (this._rfbVeNCryptState
== 3) {
1430 if (this._sock
.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength
)) { return false; }
1432 const subtypes
= [];
1433 for (let i
= 0; i
< this._rfbVeNCryptSubtypesLength
; i
++) {
1434 subtypes
.push(this._sock
.rQshift32());
1437 // 256 = Plain subtype
1438 if (subtypes
.indexOf(256) != -1) {
1440 this._sock
.send([0, 0, 1, 0]);
1441 this._rfbVeNCryptState
= 4;
1443 return this._fail("VeNCrypt Plain subtype not offered by server");
1447 // negotiated Plain subtype, server waits for password
1448 if (this._rfbVeNCryptState
== 4) {
1449 if (!this._rfbCredentials
.username
||
1450 !this._rfbCredentials
.password
) {
1451 this.dispatchEvent(new CustomEvent(
1452 "credentialsrequired",
1453 { detail
: { types
: ["username", "password"] } }));
1457 const user
= encodeUTF8(this._rfbCredentials
.username
);
1458 const pass
= encodeUTF8(this._rfbCredentials
.password
);
1460 // XXX we assume lengths are <= 255 (should not be an issue in the real world)
1461 this._sock
.send([0, 0, 0, user
.length
]);
1462 this._sock
.send([0, 0, 0, pass
.length
]);
1463 this._sock
.sendString(user
);
1464 this._sock
.sendString(pass
);
1466 this._rfbInitState
= "SecurityResult";
1471 _negotiateStdVNCAuth() {
1472 if (this._sock
.rQwait("auth challenge", 16)) { return false; }
1474 if (this._rfbCredentials
.password
=== undefined) {
1475 this.dispatchEvent(new CustomEvent(
1476 "credentialsrequired",
1477 { detail
: { types
: ["password"] } }));
1481 // TODO(directxman12): make genDES not require an Array
1482 const challenge
= Array
.prototype.slice
.call(this._sock
.rQshiftBytes(16));
1483 const response
= RFB
.genDES(this._rfbCredentials
.password
, challenge
);
1484 this._sock
.send(response
);
1485 this._rfbInitState
= "SecurityResult";
1489 _negotiateTightUnixAuth() {
1490 if (this._rfbCredentials
.username
=== undefined ||
1491 this._rfbCredentials
.password
=== undefined) {
1492 this.dispatchEvent(new CustomEvent(
1493 "credentialsrequired",
1494 { detail
: { types
: ["username", "password"] } }));
1498 this._sock
.send([0, 0, 0, this._rfbCredentials
.username
.length
]);
1499 this._sock
.send([0, 0, 0, this._rfbCredentials
.password
.length
]);
1500 this._sock
.sendString(this._rfbCredentials
.username
);
1501 this._sock
.sendString(this._rfbCredentials
.password
);
1502 this._rfbInitState
= "SecurityResult";
1506 _negotiateTightTunnels(numTunnels
) {
1507 const clientSupportedTunnelTypes
= {
1508 0: { vendor
: 'TGHT', signature
: 'NOTUNNEL' }
1510 const serverSupportedTunnelTypes
= {};
1511 // receive tunnel capabilities
1512 for (let i
= 0; i
< numTunnels
; i
++) {
1513 const capCode
= this._sock
.rQshift32();
1514 const capVendor
= this._sock
.rQshiftStr(4);
1515 const capSignature
= this._sock
.rQshiftStr(8);
1516 serverSupportedTunnelTypes
[capCode
] = { vendor
: capVendor
, signature
: capSignature
};
1519 Log
.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes
);
1521 // Siemens touch panels have a VNC server that supports NOTUNNEL,
1522 // but forgets to advertise it. Try to detect such servers by
1523 // looking for their custom tunnel type.
1524 if (serverSupportedTunnelTypes
[1] &&
1525 (serverSupportedTunnelTypes
[1].vendor
=== "SICR") &&
1526 (serverSupportedTunnelTypes
[1].signature
=== "SCHANNEL")) {
1527 Log
.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
1528 serverSupportedTunnelTypes
[0] = { vendor
: 'TGHT', signature
: 'NOTUNNEL' };
1531 // choose the notunnel type
1532 if (serverSupportedTunnelTypes
[0]) {
1533 if (serverSupportedTunnelTypes
[0].vendor
!= clientSupportedTunnelTypes
[0].vendor
||
1534 serverSupportedTunnelTypes
[0].signature
!= clientSupportedTunnelTypes
[0].signature
) {
1535 return this._fail("Client's tunnel type had the incorrect " +
1536 "vendor or signature");
1538 Log
.Debug("Selected tunnel type: " + clientSupportedTunnelTypes
[0]);
1539 this._sock
.send([0, 0, 0, 0]); // use NOTUNNEL
1540 return false; // wait until we receive the sub auth count to continue
1542 return this._fail("Server wanted tunnels, but doesn't support " +
1543 "the notunnel type");
1547 _negotiateTightAuth() {
1548 if (!this._rfbTightVNC
) { // first pass, do the tunnel negotiation
1549 if (this._sock
.rQwait("num tunnels", 4)) { return false; }
1550 const numTunnels
= this._sock
.rQshift32();
1551 if (numTunnels
> 0 && this._sock
.rQwait("tunnel capabilities", 16 * numTunnels
, 4)) { return false; }
1553 this._rfbTightVNC
= true;
1555 if (numTunnels
> 0) {
1556 this._negotiateTightTunnels(numTunnels
);
1557 return false; // wait until we receive the sub auth to continue
1561 // second pass, do the sub-auth negotiation
1562 if (this._sock
.rQwait("sub auth count", 4)) { return false; }
1563 const subAuthCount
= this._sock
.rQshift32();
1564 if (subAuthCount
=== 0) { // empty sub-auth list received means 'no auth' subtype selected
1565 this._rfbInitState
= 'SecurityResult';
1569 if (this._sock
.rQwait("sub auth capabilities", 16 * subAuthCount
, 4)) { return false; }
1571 const clientSupportedTypes
= {
1577 const serverSupportedTypes
= [];
1579 for (let i
= 0; i
< subAuthCount
; i
++) {
1580 this._sock
.rQshift32(); // capNum
1581 const capabilities
= this._sock
.rQshiftStr(12);
1582 serverSupportedTypes
.push(capabilities
);
1585 Log
.Debug("Server Tight authentication types: " + serverSupportedTypes
);
1587 for (let authType
in clientSupportedTypes
) {
1588 if (serverSupportedTypes
.indexOf(authType
) != -1) {
1589 this._sock
.send([0, 0, 0, clientSupportedTypes
[authType
]]);
1590 Log
.Debug("Selected authentication type: " + authType
);
1593 case 'STDVNOAUTH__': // no auth
1594 this._rfbInitState
= 'SecurityResult';
1596 case 'STDVVNCAUTH_': // VNC auth
1597 this._rfbAuthScheme
= 2;
1598 return this._initMsg();
1599 case 'TGHTULGNAUTH': // UNIX auth
1600 this._rfbAuthScheme
= 129;
1601 return this._initMsg();
1603 return this._fail("Unsupported tiny auth scheme " +
1604 "(scheme: " + authType
+ ")");
1609 return this._fail("No supported sub-auth types!");
1612 _negotiateAuthentication() {
1613 switch (this._rfbAuthScheme
) {
1615 if (this._rfbVersion
>= 3.8) {
1616 this._rfbInitState
= 'SecurityResult';
1619 this._rfbInitState
= 'ClientInitialisation';
1620 return this._initMsg();
1622 case 22: // XVP auth
1623 return this._negotiateXvpAuth();
1625 case 2: // VNC authentication
1626 return this._negotiateStdVNCAuth();
1628 case 16: // TightVNC Security Type
1629 return this._negotiateTightAuth();
1631 case 19: // VeNCrypt Security Type
1632 return this._negotiateVeNCryptAuth();
1634 case 129: // TightVNC UNIX Security Type
1635 return this._negotiateTightUnixAuth();
1638 return this._fail("Unsupported auth scheme (scheme: " +
1639 this._rfbAuthScheme
+ ")");
1643 _handleSecurityResult() {
1644 if (this._sock
.rQwait('VNC auth response ', 4)) { return false; }
1646 const status
= this._sock
.rQshift32();
1648 if (status
=== 0) { // OK
1649 this._rfbInitState
= 'ClientInitialisation';
1650 Log
.Debug('Authentication OK');
1651 return this._initMsg();
1653 if (this._rfbVersion
>= 3.8) {
1654 this._rfbInitState
= "SecurityReason";
1655 this._securityContext
= "security result";
1656 this._securityStatus
= status
;
1657 return this._initMsg();
1659 this.dispatchEvent(new CustomEvent(
1661 { detail
: { status
: status
} }));
1663 return this._fail("Security handshake failed");
1668 _negotiateServerInit() {
1669 if (this._sock
.rQwait("server initialization", 24)) { return false; }
1672 const width
= this._sock
.rQshift16();
1673 const height
= this._sock
.rQshift16();
1676 const bpp
= this._sock
.rQshift8();
1677 const depth
= this._sock
.rQshift8();
1678 const bigEndian
= this._sock
.rQshift8();
1679 const trueColor
= this._sock
.rQshift8();
1681 const redMax
= this._sock
.rQshift16();
1682 const greenMax
= this._sock
.rQshift16();
1683 const blueMax
= this._sock
.rQshift16();
1684 const redShift
= this._sock
.rQshift8();
1685 const greenShift
= this._sock
.rQshift8();
1686 const blueShift
= this._sock
.rQshift8();
1687 this._sock
.rQskipBytes(3); // padding
1689 // NB(directxman12): we don't want to call any callbacks or print messages until
1690 // *after* we're past the point where we could backtrack
1692 /* Connection name/title */
1693 const nameLength = this._sock.rQshift32();
1694 if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
1695 let name = this._sock.rQshiftStr(nameLength);
1696 name = decodeUTF8(name, true);
1698 if (this._rfbTightVNC) {
1699 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
1700 // In TightVNC mode, ServerInit message is extended
1701 const numServerMessages = this._sock.rQshift16();
1702 const numClientMessages = this._sock.rQshift16();
1703 const numEncodings = this._sock.rQshift16();
1704 this._sock.rQskipBytes(2); // padding
1706 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
1707 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
1709 // we don't actually do anything with the capability information that TIGHT sends,
1710 // so we just skip the all of this.
1712 // TIGHT server message capabilities
1713 this._sock.rQskipBytes(16 * numServerMessages);
1715 // TIGHT client message capabilities
1716 this._sock.rQskipBytes(16 * numClientMessages);
1718 // TIGHT encoding capabilities
1719 this._sock.rQskipBytes(16 * numEncodings);
1722 // NB(directxman12): these are down here so that we don't run them multiple times
1724 Log.Info("Screen: " + width + "x" + height +
1725 ", bpp: " + bpp + ", depth: " + depth +
1726 ", bigEndian: " + bigEndian +
1727 ", trueColor: " + trueColor +
1728 ", redMax: " + redMax +
1729 ", greenMax: " + greenMax +
1730 ", blueMax: " + blueMax +
1731 ", redShift: " + redShift +
1732 ", greenShift: " + greenShift +
1733 ", blueShift: " + blueShift);
1735 // we're past the point where we could backtrack, so it's safe to call this
1736 this._setDesktopName(name);
1737 this._resize(width, height);
1739 if (!this._viewOnly) { this._keyboard.grab(); }
1743 if (this._fbName === "Intel(r) AMT KVM") {
1744 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
1748 RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
1749 this._sendEncodings();
1750 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
1752 this._updateConnectionState('connected');
1759 // In preference order
1760 encs.push(encodings.encodingCopyRect);
1761 // Only supported with full depth support
1762 if (this._fbDepth == 24) {
1763 encs.push(encodings.encodingTight);
1764 encs.push(encodings.encodingTightPNG);
1765 encs.push(encodings.encodingHextile);
1766 encs.push(encodings.encodingRRE);
1768 encs.push(encodings.encodingRaw);
1770 // Psuedo-encoding settings
1771 encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
1772 encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
1774 encs.push(encodings.pseudoEncodingDesktopSize);
1775 encs.push(encodings.pseudoEncodingLastRect);
1776 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
1777 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
1778 encs.push(encodings.pseudoEncodingXvp);
1779 encs.push(encodings.pseudoEncodingFence);
1780 encs.push(encodings.pseudoEncodingContinuousUpdates);
1781 encs.push(encodings.pseudoEncodingDesktopName);
1782 encs.push(encodings.pseudoEncodingExtendedClipboard);
1784 if (this._fbDepth == 24) {
1785 encs.push(encodings.pseudoEncodingVMwareCursor);
1786 encs.push(encodings.pseudoEncodingCursor);
1789 RFB.messages.clientEncodings(this._sock, encs);
1792 /* RFB protocol initialization states:
1797 * ClientInitialization - not triggered by server message
1798 * ServerInitialization
1801 switch (this._rfbInitState
) {
1802 case 'ProtocolVersion':
1803 return this._negotiateProtocolVersion();
1806 return this._negotiateSecurity();
1808 case 'Authentication':
1809 return this._negotiateAuthentication();
1811 case 'SecurityResult':
1812 return this._handleSecurityResult();
1814 case 'SecurityReason':
1815 return this._handleSecurityReason();
1817 case 'ClientInitialisation':
1818 this._sock
.send([this._shared
? 1 : 0]); // ClientInitialisation
1819 this._rfbInitState
= 'ServerInitialisation';
1822 case 'ServerInitialisation':
1823 return this._negotiateServerInit();
1826 return this._fail("Unknown init state (state: " +
1827 this._rfbInitState
+ ")");
1831 _handleSetColourMapMsg() {
1832 Log
.Debug("SetColorMapEntries");
1834 return this._fail("Unexpected SetColorMapEntries message");
1837 _handleServerCutText() {
1838 Log
.Debug("ServerCutText");
1840 if (this._sock
.rQwait("ServerCutText header", 7, 1)) { return false; }
1842 this._sock
.rQskipBytes(3); // Padding
1844 let length
= this._sock
.rQshift32();
1845 length
= toSigned32bit(length
);
1847 if (this._sock
.rQwait("ServerCutText content", Math
.abs(length
), 8)) { return false; }
1851 const text
= this._sock
.rQshiftStr(length
);
1852 if (this._viewOnly
) {
1856 this.dispatchEvent(new CustomEvent(
1858 { detail
: { text
: text
} }));
1862 length
= Math
.abs(length
);
1863 const flags
= this._sock
.rQshift32();
1864 let formats
= flags
& 0x0000FFFF;
1865 let actions
= flags
& 0xFF000000;
1867 let isCaps
= (!!(actions
& extendedClipboardActionCaps
));
1869 this._clipboardServerCapabilitiesFormats
= {};
1870 this._clipboardServerCapabilitiesActions
= {};
1872 // Update our server capabilities for Formats
1873 for (let i
= 0; i
<= 15; i
++) {
1876 // Check if format flag is set.
1877 if ((formats
& index
)) {
1878 this._clipboardServerCapabilitiesFormats
[index
] = true;
1879 // We don't send unsolicited clipboard, so we
1881 this._sock
.rQshift32();
1885 // Update our server capabilities for Actions
1886 for (let i
= 24; i
<= 31; i
++) {
1888 this._clipboardServerCapabilitiesActions
[index
] = !!(actions
& index
);
1891 /* Caps handling done, send caps with the clients
1892 capabilities set as a response */
1893 let clientActions
= [
1894 extendedClipboardActionCaps
,
1895 extendedClipboardActionRequest
,
1896 extendedClipboardActionPeek
,
1897 extendedClipboardActionNotify
,
1898 extendedClipboardActionProvide
1900 RFB
.messages
.extendedClipboardCaps(this._sock
, clientActions
, {extendedClipboardFormatText
: 0});
1902 } else if (actions
=== extendedClipboardActionRequest
) {
1903 if (this._viewOnly
) {
1907 // Check if server has told us it can handle Provide and there is clipboard data to send.
1908 if (this._clipboardText
!= null &&
1909 this._clipboardServerCapabilitiesActions
[extendedClipboardActionProvide
]) {
1911 if (formats
& extendedClipboardFormatText
) {
1912 RFB
.messages
.extendedClipboardProvide(this._sock
, [extendedClipboardFormatText
], [this._clipboardText
]);
1916 } else if (actions
=== extendedClipboardActionPeek
) {
1917 if (this._viewOnly
) {
1921 if (this._clipboardServerCapabilitiesActions
[extendedClipboardActionNotify
]) {
1923 if (this._clipboardText
!= null) {
1924 RFB
.messages
.extendedClipboardNotify(this._sock
, [extendedClipboardFormatText
]);
1926 RFB
.messages
.extendedClipboardNotify(this._sock
, []);
1930 } else if (actions
=== extendedClipboardActionNotify
) {
1931 if (this._viewOnly
) {
1935 if (this._clipboardServerCapabilitiesActions
[extendedClipboardActionRequest
]) {
1937 if (formats
& extendedClipboardFormatText
) {
1938 RFB
.messages
.extendedClipboardRequest(this._sock
, [extendedClipboardFormatText
]);
1942 } else if (actions
=== extendedClipboardActionProvide
) {
1943 if (this._viewOnly
) {
1947 if (!(formats
& extendedClipboardFormatText
)) {
1950 // Ignore what we had in our clipboard client side.
1951 this._clipboardText
= null;
1953 // FIXME: Should probably verify that this data was actually requested
1954 let zlibStream
= this._sock
.rQshiftBytes(length
- 4);
1955 let streamInflator
= new Inflator();
1956 let textData
= null;
1958 streamInflator
.setInput(zlibStream
);
1959 for (let i
= 0; i
<= 15; i
++) {
1960 let format
= 1 << i
;
1962 if (formats
& format
) {
1965 let sizeArray
= streamInflator
.inflate(4);
1967 size
|= (sizeArray
[0] << 24);
1968 size
|= (sizeArray
[1] << 16);
1969 size
|= (sizeArray
[2] << 8);
1970 size
|= (sizeArray
[3]);
1971 let chunk
= streamInflator
.inflate(size
);
1973 if (format
=== extendedClipboardFormatText
) {
1978 streamInflator
.setInput(null);
1980 if (textData
!== null) {
1982 for (let i
= 0; i
< textData
.length
; i
++) {
1983 tmpText
+= String
.fromCharCode(textData
[i
]);
1987 textData
= decodeUTF8(textData
);
1988 if ((textData
.length
> 0) && "\0" === textData
.charAt(textData
.length
- 1)) {
1989 textData
= textData
.slice(0, -1);
1992 textData
= textData
.replace("\r\n", "\n");
1994 this.dispatchEvent(new CustomEvent(
1996 { detail
: { text
: textData
} }));
1999 return this._fail("Unexpected action in extended clipboard message: " + actions
);
2005 _handleServerFenceMsg() {
2006 if (this._sock
.rQwait("ServerFence header", 8, 1)) { return false; }
2007 this._sock
.rQskipBytes(3); // Padding
2008 let flags
= this._sock
.rQshift32();
2009 let length
= this._sock
.rQshift8();
2011 if (this._sock
.rQwait("ServerFence payload", length
, 9)) { return false; }
2014 Log
.Warn("Bad payload length (" + length
+ ") in fence response");
2018 const payload
= this._sock
.rQshiftStr(length
);
2020 this._supportsFence
= true;
2025 * (1<<0) - BlockBefore
2026 * (1<<1) - BlockAfter
2031 if (!(flags
& (1<<31))) {
2032 return this._fail("Unexpected fence response");
2035 // Filter out unsupported flags
2036 // FIXME: support syncNext
2037 flags
&= (1<<0) | (1<<1);
2039 // BlockBefore and BlockAfter are automatically handled by
2040 // the fact that we process each incoming message
2042 RFB
.messages
.clientFence(this._sock
, flags
, payload
);
2048 if (this._sock
.rQwait("XVP version and message", 3, 1)) { return false; }
2049 this._sock
.rQskipBytes(1); // Padding
2050 const xvpVer
= this._sock
.rQshift8();
2051 const xvpMsg
= this._sock
.rQshift8();
2055 Log
.Error("XVP Operation Failed");
2058 this._rfbXvpVer
= xvpVer
;
2059 Log
.Info("XVP extensions enabled (version " + this._rfbXvpVer
+ ")");
2060 this._setCapability("power", true);
2063 this._fail("Illegal server XVP message (msg: " + xvpMsg
+ ")");
2072 if (this._FBU
.rects
> 0) {
2075 msgType
= this._sock
.rQshift8();
2080 case 0: // FramebufferUpdate
2081 ret
= this._framebufferUpdate();
2082 if (ret
&& !this._enabledContinuousUpdates
) {
2083 RFB
.messages
.fbUpdateRequest(this._sock
, true, 0, 0,
2084 this._fbWidth
, this._fbHeight
);
2088 case 1: // SetColorMapEntries
2089 return this._handleSetColourMapMsg();
2093 this.dispatchEvent(new CustomEvent(
2098 case 3: // ServerCutText
2099 return this._handleServerCutText();
2101 case 150: // EndOfContinuousUpdates
2102 first
= !this._supportsContinuousUpdates
;
2103 this._supportsContinuousUpdates
= true;
2104 this._enabledContinuousUpdates
= false;
2106 this._enabledContinuousUpdates
= true;
2107 this._updateContinuousUpdates();
2108 Log
.Info("Enabling continuous updates.");
2110 // FIXME: We need to send a framebufferupdaterequest here
2111 // if we add support for turning off continuous updates
2115 case 248: // ServerFence
2116 return this._handleServerFenceMsg();
2119 return this._handleXvpMsg();
2122 this._fail("Unexpected server message (type " + msgType
+ ")");
2123 Log
.Debug("sock.rQslice(0, 30): " + this._sock
.rQslice(0, 30));
2129 this._flushing
= false;
2130 // Resume processing
2131 if (this._sock
.rQlen
> 0) {
2132 this._handleMessage();
2136 _framebufferUpdate() {
2137 if (this._FBU
.rects
=== 0) {
2138 if (this._sock
.rQwait("FBU header", 3, 1)) { return false; }
2139 this._sock
.rQskipBytes(1); // Padding
2140 this._FBU
.rects
= this._sock
.rQshift16();
2142 // Make sure the previous frame is fully rendered first
2143 // to avoid building up an excessive queue
2144 if (this._display
.pending()) {
2145 this._flushing
= true;
2146 this._display
.flush();
2151 while (this._FBU
.rects
> 0) {
2152 if (this._FBU
.encoding
=== null) {
2153 if (this._sock
.rQwait("rect header", 12)) { return false; }
2154 /* New FramebufferUpdate */
2156 const hdr
= this._sock
.rQshiftBytes(12);
2157 this._FBU
.x
= (hdr
[0] << 8) + hdr
[1];
2158 this._FBU
.y
= (hdr
[2] << 8) + hdr
[3];
2159 this._FBU
.width
= (hdr
[4] << 8) + hdr
[5];
2160 this._FBU
.height
= (hdr
[6] << 8) + hdr
[7];
2161 this._FBU
.encoding
= parseInt((hdr
[8] << 24) + (hdr
[9] << 16) +
2162 (hdr
[10] << 8) + hdr
[11], 10);
2165 if (!this._handleRect()) {
2170 this._FBU
.encoding
= null;
2173 this._display
.flip();
2175 return true; // We finished this FBU
2179 switch (this._FBU
.encoding
) {
2180 case encodings
.pseudoEncodingLastRect
:
2181 this._FBU
.rects
= 1; // Will be decreased when we return
2184 case encodings
.pseudoEncodingVMwareCursor
:
2185 return this._handleVMwareCursor();
2187 case encodings
.pseudoEncodingCursor
:
2188 return this._handleCursor();
2190 case encodings
.pseudoEncodingQEMUExtendedKeyEvent
:
2191 // Old Safari doesn't support creating keyboard events
2193 const keyboardEvent
= document
.createEvent("keyboardEvent");
2194 if (keyboardEvent
.code
!== undefined) {
2195 this._qemuExtKeyEventSupported
= true;
2202 case encodings
.pseudoEncodingDesktopName
:
2203 return this._handleDesktopName();
2205 case encodings
.pseudoEncodingDesktopSize
:
2206 this._resize(this._FBU
.width
, this._FBU
.height
);
2209 case encodings
.pseudoEncodingExtendedDesktopSize
:
2210 return this._handleExtendedDesktopSize();
2213 return this._handleDataRect();
2217 _handleVMwareCursor() {
2218 const hotx
= this._FBU
.x
; // hotspot-x
2219 const hoty
= this._FBU
.y
; // hotspot-y
2220 const w
= this._FBU
.width
;
2221 const h
= this._FBU
.height
;
2222 if (this._sock
.rQwait("VMware cursor encoding", 1)) {
2226 const cursorType
= this._sock
.rQshift8();
2228 this._sock
.rQshift8(); //Padding
2231 const bytesPerPixel
= 4;
2234 if (cursorType
== 0) {
2235 //Used to filter away unimportant bits.
2236 //OR is used for correct conversion in js.
2237 const PIXEL_MASK
= 0xffffff00 | 0;
2238 rgba
= new Array(w
* h
* bytesPerPixel
);
2240 if (this._sock
.rQwait("VMware cursor classic encoding",
2241 (w
* h
* bytesPerPixel
) * 2, 2)) {
2245 let andMask
= new Array(w
* h
);
2246 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2247 andMask
[pixel
] = this._sock
.rQshift32();
2250 let xorMask
= new Array(w
* h
);
2251 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2252 xorMask
[pixel
] = this._sock
.rQshift32();
2255 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2256 if (andMask
[pixel
] == 0) {
2257 //Fully opaque pixel
2258 let bgr
= xorMask
[pixel
];
2259 let r
= bgr
>> 8 & 0xff;
2260 let g
= bgr
>> 16 & 0xff;
2261 let b
= bgr
>> 24 & 0xff;
2263 rgba
[(pixel
* bytesPerPixel
) ] = r
; //r
2264 rgba
[(pixel
* bytesPerPixel
) + 1 ] = g
; //g
2265 rgba
[(pixel
* bytesPerPixel
) + 2 ] = b
; //b
2266 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff; //a
2268 } else if ((andMask
[pixel
] & PIXEL_MASK
) ==
2270 //Only screen value matters, no mouse colouring
2271 if (xorMask
[pixel
] == 0) {
2273 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2274 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2275 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2276 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0x00;
2278 } else if ((xorMask
[pixel
] & PIXEL_MASK
) ==
2280 //Inverted pixel, not supported in browsers.
2281 //Fully opaque instead.
2282 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2283 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2284 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2285 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2289 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2290 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2291 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2292 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2297 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
2298 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
2299 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
2300 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
2305 } else if (cursorType
== 1) {
2306 if (this._sock
.rQwait("VMware cursor alpha encoding",
2311 rgba
= new Array(w
* h
* bytesPerPixel
);
2313 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
2314 let data
= this._sock
.rQshift32();
2316 rgba
[(pixel
* 4) ] = data
>> 24 & 0xff; //r
2317 rgba
[(pixel
* 4) + 1 ] = data
>> 16 & 0xff; //g
2318 rgba
[(pixel
* 4) + 2 ] = data
>> 8 & 0xff; //b
2319 rgba
[(pixel
* 4) + 3 ] = data
& 0xff; //a
2323 Log
.Warn("The given cursor type is not supported: "
2324 + cursorType
+ " given.");
2328 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
2334 const hotx
= this._FBU
.x
; // hotspot-x
2335 const hoty
= this._FBU
.y
; // hotspot-y
2336 const w
= this._FBU
.width
;
2337 const h
= this._FBU
.height
;
2339 const pixelslength
= w
* h
* 4;
2340 const masklength
= Math
.ceil(w
/ 8) * h
;
2342 let bytes
= pixelslength
+ masklength
;
2343 if (this._sock
.rQwait("cursor encoding", bytes
)) {
2347 // Decode from BGRX pixels + bit mask to RGBA
2348 const pixels
= this._sock
.rQshiftBytes(pixelslength
);
2349 const mask
= this._sock
.rQshiftBytes(masklength
);
2350 let rgba
= new Uint8Array(w
* h
* 4);
2353 for (let y
= 0; y
< h
; y
++) {
2354 for (let x
= 0; x
< w
; x
++) {
2355 let maskIdx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/ 8);
2356 let alpha
= (mask
[maskIdx
] << (x
% 8)) & 0x80 ? 255 : 0;
2357 rgba
[pixIdx
] = pixels
[pixIdx
+ 2];
2358 rgba
[pixIdx
+ 1] = pixels
[pixIdx
+ 1];
2359 rgba
[pixIdx
+ 2] = pixels
[pixIdx
];
2360 rgba
[pixIdx
+ 3] = alpha
;
2365 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
2370 _handleDesktopName() {
2371 if (this._sock
.rQwait("DesktopName", 4)) {
2375 let length
= this._sock
.rQshift32();
2377 if (this._sock
.rQwait("DesktopName", length
, 4)) {
2381 let name
= this._sock
.rQshiftStr(length
);
2382 name
= decodeUTF8(name
, true);
2384 this._setDesktopName(name
);
2389 _handleExtendedDesktopSize() {
2390 if (this._sock
.rQwait("ExtendedDesktopSize", 4)) {
2394 const numberOfScreens
= this._sock
.rQpeek8();
2396 let bytes
= 4 + (numberOfScreens
* 16);
2397 if (this._sock
.rQwait("ExtendedDesktopSize", bytes
)) {
2401 const firstUpdate
= !this._supportsSetDesktopSize
;
2402 this._supportsSetDesktopSize
= true;
2404 // Normally we only apply the current resize mode after a
2405 // window resize event. However there is no such trigger on the
2406 // initial connect. And we don't know if the server supports
2407 // resizing until we've gotten here.
2409 this._requestRemoteResize();
2412 this._sock
.rQskipBytes(1); // number-of-screens
2413 this._sock
.rQskipBytes(3); // padding
2415 for (let i
= 0; i
< numberOfScreens
; i
+= 1) {
2416 // Save the id and flags of the first screen
2418 this._screenID
= this._sock
.rQshiftBytes(4); // id
2419 this._sock
.rQskipBytes(2); // x-position
2420 this._sock
.rQskipBytes(2); // y-position
2421 this._sock
.rQskipBytes(2); // width
2422 this._sock
.rQskipBytes(2); // height
2423 this._screenFlags
= this._sock
.rQshiftBytes(4); // flags
2425 this._sock
.rQskipBytes(16);
2430 * The x-position indicates the reason for the change:
2432 * 0 - server resized on its own
2433 * 1 - this client requested the resize
2434 * 2 - another client requested the resize
2437 // We need to handle errors when we requested the resize.
2438 if (this._FBU
.x
=== 1 && this._FBU
.y
!== 0) {
2440 // The y-position indicates the status code from the server
2441 switch (this._FBU
.y
) {
2443 msg
= "Resize is administratively prohibited";
2446 msg
= "Out of resources";
2449 msg
= "Invalid screen layout";
2452 msg
= "Unknown reason";
2455 Log
.Warn("Server did not accept the resize request: "
2458 this._resize(this._FBU
.width
, this._FBU
.height
);
2465 let decoder
= this._decoders
[this._FBU
.encoding
];
2467 this._fail("Unsupported encoding (encoding: " +
2468 this._FBU
.encoding
+ ")");
2473 return decoder
.decodeRect(this._FBU
.x
, this._FBU
.y
,
2474 this._FBU
.width
, this._FBU
.height
,
2475 this._sock
, this._display
,
2478 this._fail("Error decoding rect: " + err
);
2483 _updateContinuousUpdates() {
2484 if (!this._enabledContinuousUpdates
) { return; }
2486 RFB
.messages
.enableContinuousUpdates(this._sock
, true, 0, 0,
2487 this._fbWidth
, this._fbHeight
);
2490 _resize(width
, height
) {
2491 this._fbWidth
= width
;
2492 this._fbHeight
= height
;
2494 this._display
.resize(this._fbWidth
, this._fbHeight
);
2496 // Adjust the visible viewport based on the new dimensions
2498 this._updateScale();
2500 this._updateContinuousUpdates();
2504 if (this._rfbXvpVer
< ver
) { return; }
2505 Log
.Info("Sending XVP operation " + op
+ " (version " + ver
+ ")");
2506 RFB
.messages
.xvpOp(this._sock
, ver
, op
);
2509 _updateCursor(rgba
, hotx
, hoty
, w
, h
) {
2510 this._cursorImage
= {
2512 hotx
: hotx
, hoty
: hoty
, w
: w
, h
: h
,
2514 this._refreshCursor();
2517 _shouldShowDotCursor() {
2518 // Called when this._cursorImage is updated
2519 if (!this._showDotCursor
) {
2520 // User does not want to see the dot, so...
2524 // The dot should not be shown if the cursor is already visible,
2525 // i.e. contains at least one not-fully-transparent pixel.
2526 // So iterate through all alpha bytes in rgba and stop at the
2528 for (let i
= 3; i
< this._cursorImage
.rgbaPixels
.length
; i
+= 4) {
2529 if (this._cursorImage
.rgbaPixels
[i
]) {
2534 // At this point, we know that the cursor is fully transparent, and
2535 // the user wants to see the dot instead of this.
2540 if (this._rfbConnectionState
!== "connecting" &&
2541 this._rfbConnectionState
!== "connected") {
2544 const image
= this._shouldShowDotCursor() ? RFB
.cursors
.dot
: this._cursorImage
;
2545 this._cursor
.change(image
.rgbaPixels
,
2546 image
.hotx
, image
.hoty
,
2551 static genDES(password
, challenge
) {
2552 const passwordChars
= password
.split('').map(c
=> c
.charCodeAt(0));
2553 return (new DES(passwordChars
)).encrypt(challenge
);
2559 keyEvent(sock
, keysym
, down
) {
2560 const buff
= sock
._sQ
;
2561 const offset
= sock
._sQlen
;
2563 buff
[offset
] = 4; // msg-type
2564 buff
[offset
+ 1] = down
;
2566 buff
[offset
+ 2] = 0;
2567 buff
[offset
+ 3] = 0;
2569 buff
[offset
+ 4] = (keysym
>> 24);
2570 buff
[offset
+ 5] = (keysym
>> 16);
2571 buff
[offset
+ 6] = (keysym
>> 8);
2572 buff
[offset
+ 7] = keysym
;
2578 QEMUExtendedKeyEvent(sock
, keysym
, down
, keycode
) {
2579 function getRFBkeycode(xtScanCode
) {
2580 const upperByte
= (keycode
>> 8);
2581 const lowerByte
= (keycode
& 0x00ff);
2582 if (upperByte
=== 0xe0 && lowerByte
< 0x7f) {
2583 return lowerByte
| 0x80;
2588 const buff
= sock
._sQ
;
2589 const offset
= sock
._sQlen
;
2591 buff
[offset
] = 255; // msg-type
2592 buff
[offset
+ 1] = 0; // sub msg-type
2594 buff
[offset
+ 2] = (down
>> 8);
2595 buff
[offset
+ 3] = down
;
2597 buff
[offset
+ 4] = (keysym
>> 24);
2598 buff
[offset
+ 5] = (keysym
>> 16);
2599 buff
[offset
+ 6] = (keysym
>> 8);
2600 buff
[offset
+ 7] = keysym
;
2602 const RFBkeycode
= getRFBkeycode(keycode
);
2604 buff
[offset
+ 8] = (RFBkeycode
>> 24);
2605 buff
[offset
+ 9] = (RFBkeycode
>> 16);
2606 buff
[offset
+ 10] = (RFBkeycode
>> 8);
2607 buff
[offset
+ 11] = RFBkeycode
;
2613 pointerEvent(sock
, x
, y
, mask
) {
2614 const buff
= sock
._sQ
;
2615 const offset
= sock
._sQlen
;
2617 buff
[offset
] = 5; // msg-type
2619 buff
[offset
+ 1] = mask
;
2621 buff
[offset
+ 2] = x
>> 8;
2622 buff
[offset
+ 3] = x
;
2624 buff
[offset
+ 4] = y
>> 8;
2625 buff
[offset
+ 5] = y
;
2631 // Used to build Notify and Request data.
2632 _buildExtendedClipboardFlags(actions
, formats
) {
2633 let data
= new Uint8Array(4);
2634 let formatFlag
= 0x00000000;
2635 let actionFlag
= 0x00000000;
2637 for (let i
= 0; i
< actions
.length
; i
++) {
2638 actionFlag
|= actions
[i
];
2641 for (let i
= 0; i
< formats
.length
; i
++) {
2642 formatFlag
|= formats
[i
];
2645 data
[0] = actionFlag
>> 24; // Actions
2646 data
[1] = 0x00; // Reserved
2647 data
[2] = 0x00; // Reserved
2648 data
[3] = formatFlag
; // Formats
2653 extendedClipboardProvide(sock
, formats
, inData
) {
2654 // Deflate incomming data and their sizes
2655 let deflator
= new Deflator();
2656 let dataToDeflate
= [];
2658 for (let i
= 0; i
< formats
.length
; i
++) {
2659 // We only support the format Text at this time
2660 if (formats
[i
] != extendedClipboardFormatText
) {
2661 throw new Error("Unsupported extended clipboard format for Provide message.");
2664 // Change lone \r or \n into \r\n as defined in rfbproto
2665 inData
[i
] = inData
[i
].replace(/\r\n|\r|\n/gm, "\r\n");
2667 // Check if it already has \0
2668 let text
= encodeUTF8(inData
[i
] + "\0");
2670 dataToDeflate
.push( (text
.length
>> 24) & 0xFF,
2671 (text
.length
>> 16) & 0xFF,
2672 (text
.length
>> 8) & 0xFF,
2673 (text
.length
& 0xFF));
2675 for (let j
= 0; j
< text
.length
; j
++) {
2676 dataToDeflate
.push(text
.charCodeAt(j
));
2680 let deflatedData
= deflator
.deflate(new Uint8Array(dataToDeflate
));
2682 // Build data to send
2683 let data
= new Uint8Array(4 + deflatedData
.length
);
2684 data
.set(RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionProvide
],
2686 data
.set(deflatedData
, 4);
2688 RFB
.messages
.clientCutText(sock
, data
, true);
2691 extendedClipboardNotify(sock
, formats
) {
2692 let flags
= RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionNotify
],
2694 RFB
.messages
.clientCutText(sock
, flags
, true);
2697 extendedClipboardRequest(sock
, formats
) {
2698 let flags
= RFB
.messages
._buildExtendedClipboardFlags([extendedClipboardActionRequest
],
2700 RFB
.messages
.clientCutText(sock
, flags
, true);
2703 extendedClipboardCaps(sock
, actions
, formats
) {
2704 let formatKeys
= Object
.keys(formats
);
2705 let data
= new Uint8Array(4 + (4 * formatKeys
.length
));
2707 formatKeys
.map(x
=> parseInt(x
));
2708 formatKeys
.sort((a
, b
) => a
- b
);
2710 data
.set(RFB
.messages
._buildExtendedClipboardFlags(actions
, []));
2713 for (let i
= 0; i
< formatKeys
.length
; i
++) {
2714 data
[loopOffset
] = formats
[formatKeys
[i
]] >> 24;
2715 data
[loopOffset
+ 1] = formats
[formatKeys
[i
]] >> 16;
2716 data
[loopOffset
+ 2] = formats
[formatKeys
[i
]] >> 8;
2717 data
[loopOffset
+ 3] = formats
[formatKeys
[i
]] >> 0;
2720 data
[3] |= (1 << formatKeys
[i
]); // Update our format flags
2723 RFB
.messages
.clientCutText(sock
, data
, true);
2726 clientCutText(sock
, data
, extended
= false) {
2727 const buff
= sock
._sQ
;
2728 const offset
= sock
._sQlen
;
2730 buff
[offset
] = 6; // msg-type
2732 buff
[offset
+ 1] = 0; // padding
2733 buff
[offset
+ 2] = 0; // padding
2734 buff
[offset
+ 3] = 0; // padding
2738 length
= toUnsigned32bit(-data
.length
);
2740 length
= data
.length
;
2743 buff
[offset
+ 4] = length
>> 24;
2744 buff
[offset
+ 5] = length
>> 16;
2745 buff
[offset
+ 6] = length
>> 8;
2746 buff
[offset
+ 7] = length
;
2750 // We have to keep track of from where in the data we begin creating the
2751 // buffer for the flush in the next iteration.
2754 let remaining
= data
.length
;
2755 while (remaining
> 0) {
2757 let flushSize
= Math
.min(remaining
, (sock
._sQbufferSize
- sock
._sQlen
));
2758 for (let i
= 0; i
< flushSize
; i
++) {
2759 buff
[sock
._sQlen
+ i
] = data
[dataOffset
+ i
];
2762 sock
._sQlen
+= flushSize
;
2765 remaining
-= flushSize
;
2766 dataOffset
+= flushSize
;
2771 setDesktopSize(sock
, width
, height
, id
, flags
) {
2772 const buff
= sock
._sQ
;
2773 const offset
= sock
._sQlen
;
2775 buff
[offset
] = 251; // msg-type
2776 buff
[offset
+ 1] = 0; // padding
2777 buff
[offset
+ 2] = width
>> 8; // width
2778 buff
[offset
+ 3] = width
;
2779 buff
[offset
+ 4] = height
>> 8; // height
2780 buff
[offset
+ 5] = height
;
2782 buff
[offset
+ 6] = 1; // number-of-screens
2783 buff
[offset
+ 7] = 0; // padding
2786 buff
[offset
+ 8] = id
>> 24; // id
2787 buff
[offset
+ 9] = id
>> 16;
2788 buff
[offset
+ 10] = id
>> 8;
2789 buff
[offset
+ 11] = id
;
2790 buff
[offset
+ 12] = 0; // x-position
2791 buff
[offset
+ 13] = 0;
2792 buff
[offset
+ 14] = 0; // y-position
2793 buff
[offset
+ 15] = 0;
2794 buff
[offset
+ 16] = width
>> 8; // width
2795 buff
[offset
+ 17] = width
;
2796 buff
[offset
+ 18] = height
>> 8; // height
2797 buff
[offset
+ 19] = height
;
2798 buff
[offset
+ 20] = flags
>> 24; // flags
2799 buff
[offset
+ 21] = flags
>> 16;
2800 buff
[offset
+ 22] = flags
>> 8;
2801 buff
[offset
+ 23] = flags
;
2807 clientFence(sock
, flags
, payload
) {
2808 const buff
= sock
._sQ
;
2809 const offset
= sock
._sQlen
;
2811 buff
[offset
] = 248; // msg-type
2813 buff
[offset
+ 1] = 0; // padding
2814 buff
[offset
+ 2] = 0; // padding
2815 buff
[offset
+ 3] = 0; // padding
2817 buff
[offset
+ 4] = flags
>> 24; // flags
2818 buff
[offset
+ 5] = flags
>> 16;
2819 buff
[offset
+ 6] = flags
>> 8;
2820 buff
[offset
+ 7] = flags
;
2822 const n
= payload
.length
;
2824 buff
[offset
+ 8] = n
; // length
2826 for (let i
= 0; i
< n
; i
++) {
2827 buff
[offset
+ 9 + i
] = payload
.charCodeAt(i
);
2830 sock
._sQlen
+= 9 + n
;
2834 enableContinuousUpdates(sock
, enable
, x
, y
, width
, height
) {
2835 const buff
= sock
._sQ
;
2836 const offset
= sock
._sQlen
;
2838 buff
[offset
] = 150; // msg-type
2839 buff
[offset
+ 1] = enable
; // enable-flag
2841 buff
[offset
+ 2] = x
>> 8; // x
2842 buff
[offset
+ 3] = x
;
2843 buff
[offset
+ 4] = y
>> 8; // y
2844 buff
[offset
+ 5] = y
;
2845 buff
[offset
+ 6] = width
>> 8; // width
2846 buff
[offset
+ 7] = width
;
2847 buff
[offset
+ 8] = height
>> 8; // height
2848 buff
[offset
+ 9] = height
;
2854 pixelFormat(sock
, depth
, trueColor
) {
2855 const buff
= sock
._sQ
;
2856 const offset
= sock
._sQlen
;
2862 } else if (depth
> 8) {
2868 const bits
= Math
.floor(depth
/3);
2870 buff
[offset
] = 0; // msg-type
2872 buff
[offset
+ 1] = 0; // padding
2873 buff
[offset
+ 2] = 0; // padding
2874 buff
[offset
+ 3] = 0; // padding
2876 buff
[offset
+ 4] = bpp
; // bits-per-pixel
2877 buff
[offset
+ 5] = depth
; // depth
2878 buff
[offset
+ 6] = 0; // little-endian
2879 buff
[offset
+ 7] = trueColor
? 1 : 0; // true-color
2881 buff
[offset
+ 8] = 0; // red-max
2882 buff
[offset
+ 9] = (1 << bits
) - 1; // red-max
2884 buff
[offset
+ 10] = 0; // green-max
2885 buff
[offset
+ 11] = (1 << bits
) - 1; // green-max
2887 buff
[offset
+ 12] = 0; // blue-max
2888 buff
[offset
+ 13] = (1 << bits
) - 1; // blue-max
2890 buff
[offset
+ 14] = bits
* 2; // red-shift
2891 buff
[offset
+ 15] = bits
* 1; // green-shift
2892 buff
[offset
+ 16] = bits
* 0; // blue-shift
2894 buff
[offset
+ 17] = 0; // padding
2895 buff
[offset
+ 18] = 0; // padding
2896 buff
[offset
+ 19] = 0; // padding
2902 clientEncodings(sock
, encodings
) {
2903 const buff
= sock
._sQ
;
2904 const offset
= sock
._sQlen
;
2906 buff
[offset
] = 2; // msg-type
2907 buff
[offset
+ 1] = 0; // padding
2909 buff
[offset
+ 2] = encodings
.length
>> 8;
2910 buff
[offset
+ 3] = encodings
.length
;
2913 for (let i
= 0; i
< encodings
.length
; i
++) {
2914 const enc
= encodings
[i
];
2915 buff
[j
] = enc
>> 24;
2916 buff
[j
+ 1] = enc
>> 16;
2917 buff
[j
+ 2] = enc
>> 8;
2923 sock
._sQlen
+= j
- offset
;
2927 fbUpdateRequest(sock
, incremental
, x
, y
, w
, h
) {
2928 const buff
= sock
._sQ
;
2929 const offset
= sock
._sQlen
;
2931 if (typeof(x
) === "undefined") { x
= 0; }
2932 if (typeof(y
) === "undefined") { y
= 0; }
2934 buff
[offset
] = 3; // msg-type
2935 buff
[offset
+ 1] = incremental
? 1 : 0;
2937 buff
[offset
+ 2] = (x
>> 8) & 0xFF;
2938 buff
[offset
+ 3] = x
& 0xFF;
2940 buff
[offset
+ 4] = (y
>> 8) & 0xFF;
2941 buff
[offset
+ 5] = y
& 0xFF;
2943 buff
[offset
+ 6] = (w
>> 8) & 0xFF;
2944 buff
[offset
+ 7] = w
& 0xFF;
2946 buff
[offset
+ 8] = (h
>> 8) & 0xFF;
2947 buff
[offset
+ 9] = h
& 0xFF;
2953 xvpOp(sock
, ver
, op
) {
2954 const buff
= sock
._sQ
;
2955 const offset
= sock
._sQlen
;
2957 buff
[offset
] = 250; // msg-type
2958 buff
[offset
+ 1] = 0; // padding
2960 buff
[offset
+ 2] = ver
;
2961 buff
[offset
+ 3] = op
;
2970 rgbaPixels
: new Uint8Array(),
2976 /* eslint-disable indent */
2977 rgbaPixels
: new Uint8Array([
2978 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2979 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
2980 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2982 /* eslint-enable indent */