2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2019 The noVNC Authors
4 * Licensed under MPL 2.0 (see LICENSE.txt)
6 * See README.md for usage and integration instructions.
10 import * as Log
from './util/logging.js';
11 import { decodeUTF8
} from './util/strings.js';
12 import { dragThreshold
} from './util/browser.js';
13 import EventTargetMixin
from './util/eventtarget.js';
14 import Display
from "./display.js";
15 import Keyboard
from "./input/keyboard.js";
16 import Mouse
from "./input/mouse.js";
17 import Cursor
from "./util/cursor.js";
18 import Websock
from "./websock.js";
19 import DES
from "./des.js";
20 import KeyTable
from "./input/keysym.js";
21 import XtScancode
from "./input/xtscancodes.js";
22 import { encodings
} from "./encodings.js";
23 import "./util/polyfill.js";
25 import RawDecoder
from "./decoders/raw.js";
26 import CopyRectDecoder
from "./decoders/copyrect.js";
27 import RREDecoder
from "./decoders/rre.js";
28 import HextileDecoder
from "./decoders/hextile.js";
29 import TightDecoder
from "./decoders/tight.js";
30 import TightPNGDecoder
from "./decoders/tightpng.js";
32 // How many seconds to wait for a disconnect to finish
33 const DISCONNECT_TIMEOUT
= 3;
34 const DEFAULT_BACKGROUND
= 'rgb(40, 40, 40)';
36 export default class RFB
extends EventTargetMixin
{
37 constructor(target
, url
, options
) {
39 throw new Error("Must specify target");
42 throw new Error("Must specify URL");
47 this._target
= target
;
51 options
= options
|| {};
52 this._rfb_credentials
= options
.credentials
|| {};
53 this._shared
= 'shared' in options
? !!options
.shared
: true;
54 this._repeaterID
= options
.repeaterID
|| '';
55 this._wsProtocols
= options
.wsProtocols
|| [];
58 this._rfb_connection_state
= '';
59 this._rfb_init_state
= '';
60 this._rfb_auth_scheme
= -1;
61 this._rfb_clean_disconnect
= true;
63 // Server capabilities
64 this._rfb_version
= 0;
65 this._rfb_max_version
= 3.8;
66 this._rfb_tightvnc
= false;
67 this._rfb_xvp_ver
= 0;
74 this._capabilities
= { power
: false };
76 this._supportsFence
= false;
78 this._supportsContinuousUpdates
= false;
79 this._enabledContinuousUpdates
= false;
81 this._supportsSetDesktopSize
= false;
83 this._screen_flags
= 0;
85 this._qemuExtKeyEventSupported
= false;
88 this._sock
= null; // Websock object
89 this._display
= null; // Display object
90 this._flushing
= false; // Display flushing state
91 this._keyboard
= null; // Keyboard input handler object
92 this._mouse
= null; // Mouse input handler object
95 this._disconnTimer
= null; // disconnection timer
96 this._resizeTimeout
= null; // resize rate limiting
111 this._mouse_buttonMask
= 0;
112 this._mouse_arr
= [];
113 this._viewportDragging
= false;
114 this._viewportDragPos
= {};
115 this._viewportHasMoved
= false;
117 // Bound event handlers
118 this._eventHandlers
= {
119 focusCanvas
: this._focusCanvas
.bind(this),
120 windowResize
: this._windowResize
.bind(this),
124 Log
.Debug(">> RFB.constructor");
126 // Create DOM elements
127 this._screen
= document
.createElement('div');
128 this._screen
.style
.display
= 'flex';
129 this._screen
.style
.width
= '100%';
130 this._screen
.style
.height
= '100%';
131 this._screen
.style
.overflow
= 'auto';
132 this._screen
.style
.background
= DEFAULT_BACKGROUND
;
133 this._canvas
= document
.createElement('canvas');
134 this._canvas
.style
.margin
= 'auto';
135 // Some browsers add an outline on focus
136 this._canvas
.style
.outline
= 'none';
137 // IE miscalculates width without this :(
138 this._canvas
.style
.flexShrink
= '0';
139 this._canvas
.width
= 0;
140 this._canvas
.height
= 0;
141 this._canvas
.tabIndex
= -1;
142 this._screen
.appendChild(this._canvas
);
145 this._cursor
= new Cursor();
147 // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
148 // it. Result: no cursor at all until a window border or an edit field
149 // is hit blindly. But there are also VNC servers that draw the cursor
150 // in the framebuffer and don't send the empty local cursor. There is
151 // no way to satisfy both sides.
153 // The spec is unclear on this "initial cursor" issue. Many other
154 // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
155 // initial cursor instead.
156 this._cursorImage
= RFB
.cursors
.none
;
158 // populate decoder array with objects
159 this._decoders
[encodings
.encodingRaw
] = new RawDecoder();
160 this._decoders
[encodings
.encodingCopyRect
] = new CopyRectDecoder();
161 this._decoders
[encodings
.encodingRRE
] = new RREDecoder();
162 this._decoders
[encodings
.encodingHextile
] = new HextileDecoder();
163 this._decoders
[encodings
.encodingTight
] = new TightDecoder();
164 this._decoders
[encodings
.encodingTightPNG
] = new TightPNGDecoder();
166 // NB: nothing that needs explicit teardown should be done
167 // before this point, since this can throw an exception
169 this._display
= new Display(this._canvas
);
171 Log
.Error("Display exception: " + exc
);
174 this._display
.onflush
= this._onFlush
.bind(this);
176 this._keyboard
= new Keyboard(this._canvas
);
177 this._keyboard
.onkeyevent
= this._handleKeyEvent
.bind(this);
179 this._mouse
= new Mouse(this._canvas
);
180 this._mouse
.onmousebutton
= this._handleMouseButton
.bind(this);
181 this._mouse
.onmousemove
= this._handleMouseMove
.bind(this);
183 this._sock
= new Websock();
184 this._sock
.on('message', () => {
185 this._handle_message();
187 this._sock
.on('open', () => {
188 if ((this._rfb_connection_state
=== 'connecting') &&
189 (this._rfb_init_state
=== '')) {
190 this._rfb_init_state
= 'ProtocolVersion';
191 Log
.Debug("Starting VNC handshake");
193 this._fail("Unexpected server connection while " +
194 this._rfb_connection_state
);
197 this._sock
.on('close', (e
) => {
198 Log
.Debug("WebSocket on-close event");
201 msg
= "(code: " + e
.code
;
203 msg
+= ", reason: " + e
.reason
;
207 switch (this._rfb_connection_state
) {
209 this._fail("Connection closed " + msg
);
212 // Handle disconnects that were initiated server-side
213 this._updateConnectionState('disconnecting');
214 this._updateConnectionState('disconnected');
216 case 'disconnecting':
217 // Normal disconnection path
218 this._updateConnectionState('disconnected');
221 this._fail("Unexpected server disconnect " +
222 "when already disconnected " + msg
);
225 this._fail("Unexpected server disconnect before connecting " +
229 this._sock
.off('close');
231 this._sock
.on('error', e
=> Log
.Warn("WebSocket on-error event"));
233 // Slight delay of the actual connection so that the caller has
234 // time to set up callbacks
235 setTimeout(this._updateConnectionState
.bind(this, 'connecting'));
237 Log
.Debug("<< RFB.constructor");
239 // ===== PROPERTIES =====
241 this.dragViewport
= false;
242 this.focusOnClick
= true;
244 this._viewOnly
= false;
245 this._clipViewport
= false;
246 this._scaleViewport
= false;
247 this._resizeSession
= false;
249 this._showDotCursor
= false;
250 if (options
.showDotCursor
!== undefined) {
251 Log
.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated");
252 this._showDotCursor
= options
.showDotCursor
;
256 // ===== PROPERTIES =====
258 get viewOnly() { return this._viewOnly
; }
259 set viewOnly(viewOnly
) {
260 this._viewOnly
= viewOnly
;
262 if (this._rfb_connection_state
=== "connecting" ||
263 this._rfb_connection_state
=== "connected") {
265 this._keyboard
.ungrab();
266 this._mouse
.ungrab();
268 this._keyboard
.grab();
274 get capabilities() { return this._capabilities
; }
276 get touchButton() { return this._mouse
.touchButton
; }
277 set touchButton(button
) { this._mouse
.touchButton
= button
; }
279 get clipViewport() { return this._clipViewport
; }
280 set clipViewport(viewport
) {
281 this._clipViewport
= viewport
;
285 get scaleViewport() { return this._scaleViewport
; }
286 set scaleViewport(scale
) {
287 this._scaleViewport
= scale
;
288 // Scaling trumps clipping, so we may need to adjust
289 // clipping when enabling or disabling scaling
290 if (scale
&& this._clipViewport
) {
294 if (!scale
&& this._clipViewport
) {
299 get resizeSession() { return this._resizeSession
; }
300 set resizeSession(resize
) {
301 this._resizeSession
= resize
;
303 this._requestRemoteResize();
307 get showDotCursor() { return this._showDotCursor
; }
308 set showDotCursor(show
) {
309 this._showDotCursor
= show
;
310 this._refreshCursor();
313 get background() { return this._screen
.style
.background
; }
314 set background(cssValue
) { this._screen
.style
.background
= cssValue
; }
316 // ===== PUBLIC METHODS =====
319 this._updateConnectionState('disconnecting');
320 this._sock
.off('error');
321 this._sock
.off('message');
322 this._sock
.off('open');
325 sendCredentials(creds
) {
326 this._rfb_credentials
= creds
;
327 setTimeout(this._init_msg
.bind(this), 0);
331 if (this._rfb_connection_state
!== 'connected' || this._viewOnly
) { return; }
332 Log
.Info("Sending Ctrl-Alt-Del");
334 this.sendKey(KeyTable
.XK_Control_L
, "ControlLeft", true);
335 this.sendKey(KeyTable
.XK_Alt_L
, "AltLeft", true);
336 this.sendKey(KeyTable
.XK_Delete
, "Delete", true);
337 this.sendKey(KeyTable
.XK_Delete
, "Delete", false);
338 this.sendKey(KeyTable
.XK_Alt_L
, "AltLeft", false);
339 this.sendKey(KeyTable
.XK_Control_L
, "ControlLeft", false);
354 // Send a key press. If 'down' is not specified then send a down key
355 // followed by an up key.
356 sendKey(keysym
, code
, down
) {
357 if (this._rfb_connection_state
!== 'connected' || this._viewOnly
) { return; }
359 if (down
=== undefined) {
360 this.sendKey(keysym
, code
, true);
361 this.sendKey(keysym
, code
, false);
365 const scancode
= XtScancode
[code
];
367 if (this._qemuExtKeyEventSupported
&& scancode
) {
369 keysym
= keysym
|| 0;
371 Log
.Info("Sending key (" + (down
? "down" : "up") + "): keysym " + keysym
+ ", scancode " + scancode
);
373 RFB
.messages
.QEMUExtendedKeyEvent(this._sock
, keysym
, down
, scancode
);
378 Log
.Info("Sending keysym (" + (down
? "down" : "up") + "): " + keysym
);
379 RFB
.messages
.keyEvent(this._sock
, keysym
, down
? 1 : 0);
384 this._canvas
.focus();
391 clipboardPasteFrom(text
) {
392 if (this._rfb_connection_state
!== 'connected' || this._viewOnly
) { return; }
393 RFB
.messages
.clientCutText(this._sock
, text
);
396 // ===== PRIVATE METHODS =====
399 Log
.Debug(">> RFB.connect");
401 Log
.Info("connecting to " + this._url
);
404 // WebSocket.onopen transitions to the RFB init states
405 this._sock
.open(this._url
, this._wsProtocols
);
407 if (e
.name
=== 'SyntaxError') {
408 this._fail("Invalid host or port (" + e
+ ")");
410 this._fail("Error when opening socket (" + e
+ ")");
414 // Make our elements part of the page
415 this._target
.appendChild(this._screen
);
417 this._cursor
.attach(this._canvas
);
418 this._refreshCursor();
420 // Monitor size changes of the screen
421 // FIXME: Use ResizeObserver, or hidden overflow
422 window
.addEventListener('resize', this._eventHandlers
.windowResize
);
424 // Always grab focus on some kind of click event
425 this._canvas
.addEventListener("mousedown", this._eventHandlers
.focusCanvas
);
426 this._canvas
.addEventListener("touchstart", this._eventHandlers
.focusCanvas
);
428 Log
.Debug("<< RFB.connect");
432 Log
.Debug(">> RFB.disconnect");
433 this._cursor
.detach();
434 this._canvas
.removeEventListener("mousedown", this._eventHandlers
.focusCanvas
);
435 this._canvas
.removeEventListener("touchstart", this._eventHandlers
.focusCanvas
);
436 window
.removeEventListener('resize', this._eventHandlers
.windowResize
);
437 this._keyboard
.ungrab();
438 this._mouse
.ungrab();
441 this._target
.removeChild(this._screen
);
443 if (e
.name
=== 'NotFoundError') {
444 // Some cases where the initial connection fails
445 // can disconnect before the _screen is created
450 clearTimeout(this._resizeTimeout
);
451 Log
.Debug("<< RFB.disconnect");
454 _focusCanvas(event
) {
455 // Respect earlier handlers' request to not do side-effects
456 if (event
.defaultPrevented
) {
460 if (!this.focusOnClick
) {
467 _setDesktopName(name
) {
468 this._fb_name
= name
;
469 this.dispatchEvent(new CustomEvent(
471 { detail
: { name
: this._fb_name
} }));
474 _windowResize(event
) {
475 // If the window resized then our screen element might have
476 // as well. Update the viewport dimensions.
477 window
.requestAnimationFrame(() => {
482 if (this._resizeSession
) {
483 // Request changing the resolution of the remote display to
484 // the size of the local browser viewport.
486 // In order to not send multiple requests before the browser-resize
487 // is finished we wait 0.5 seconds before sending the request.
488 clearTimeout(this._resizeTimeout
);
489 this._resizeTimeout
= setTimeout(this._requestRemoteResize
.bind(this), 500);
493 // Update state of clipping in Display object, and make sure the
494 // configured viewport matches the current screen size
496 const cur_clip
= this._display
.clipViewport
;
497 let new_clip
= this._clipViewport
;
499 if (this._scaleViewport
) {
500 // Disable viewport clipping if we are scaling
504 if (cur_clip
!== new_clip
) {
505 this._display
.clipViewport
= new_clip
;
509 // When clipping is enabled, the screen is limited to
510 // the size of the container.
511 const size
= this._screenSize();
512 this._display
.viewportChangeSize(size
.w
, size
.h
);
513 this._fixScrollbars();
518 if (!this._scaleViewport
) {
519 this._display
.scale
= 1.0;
521 const size
= this._screenSize();
522 this._display
.autoscale(size
.w
, size
.h
);
524 this._fixScrollbars();
527 // Requests a change of remote desktop size. This message is an extension
528 // and may only be sent if we have received an ExtendedDesktopSize message
529 _requestRemoteResize() {
530 clearTimeout(this._resizeTimeout
);
531 this._resizeTimeout
= null;
533 if (!this._resizeSession
|| this._viewOnly
||
534 !this._supportsSetDesktopSize
) {
538 const size
= this._screenSize();
539 RFB
.messages
.setDesktopSize(this._sock
,
540 Math
.floor(size
.w
), Math
.floor(size
.h
),
541 this._screen_id
, this._screen_flags
);
543 Log
.Debug('Requested new desktop size: ' +
544 size
.w
+ 'x' + size
.h
);
547 // Gets the the size of the available screen
549 let r
= this._screen
.getBoundingClientRect();
550 return { w
: r
.width
, h
: r
.height
};
554 // This is a hack because Chrome screws up the calculation
555 // for when scrollbars are needed. So to fix it we temporarily
556 // toggle them off and on.
557 const orig
= this._screen
.style
.overflow
;
558 this._screen
.style
.overflow
= 'hidden';
559 // Force Chrome to recalculate the layout by asking for
560 // an element's dimensions
561 this._screen
.getBoundingClientRect();
562 this._screen
.style
.overflow
= orig
;
570 * disconnected - permanent state
572 _updateConnectionState(state
) {
573 const oldstate
= this._rfb_connection_state
;
575 if (state
=== oldstate
) {
576 Log
.Debug("Already in state '" + state
+ "', ignoring");
580 // The 'disconnected' state is permanent for each RFB object
581 if (oldstate
=== 'disconnected') {
582 Log
.Error("Tried changing state of a disconnected RFB object");
586 // Ensure proper transitions before doing anything
589 if (oldstate
!== 'connecting') {
590 Log
.Error("Bad transition to connected state, " +
591 "previous connection state: " + oldstate
);
597 if (oldstate
!== 'disconnecting') {
598 Log
.Error("Bad transition to disconnected state, " +
599 "previous connection state: " + oldstate
);
605 if (oldstate
!== '') {
606 Log
.Error("Bad transition to connecting state, " +
607 "previous connection state: " + oldstate
);
612 case 'disconnecting':
613 if (oldstate
!== 'connected' && oldstate
!== 'connecting') {
614 Log
.Error("Bad transition to disconnecting state, " +
615 "previous connection state: " + oldstate
);
621 Log
.Error("Unknown connection state: " + state
);
625 // State change actions
627 this._rfb_connection_state
= state
;
629 Log
.Debug("New state '" + state
+ "', was '" + oldstate
+ "'.");
631 if (this._disconnTimer
&& state
!== 'disconnecting') {
632 Log
.Debug("Clearing disconnect timer");
633 clearTimeout(this._disconnTimer
);
634 this._disconnTimer
= null;
636 // make sure we don't get a double event
637 this._sock
.off('close');
646 this.dispatchEvent(new CustomEvent("connect", { detail
: {} }));
649 case 'disconnecting':
652 this._disconnTimer
= setTimeout(() => {
653 Log
.Error("Disconnection timed out.");
654 this._updateConnectionState('disconnected');
655 }, DISCONNECT_TIMEOUT
* 1000);
659 this.dispatchEvent(new CustomEvent(
660 "disconnect", { detail
:
661 { clean
: this._rfb_clean_disconnect
} }));
666 /* Print errors and disconnect
668 * The parameter 'details' is used for information that
669 * should be logged but not sent to the user interface.
672 switch (this._rfb_connection_state
) {
673 case 'disconnecting':
674 Log
.Error("Failed when disconnecting: " + details
);
677 Log
.Error("Failed while connected: " + details
);
680 Log
.Error("Failed when connecting: " + details
);
683 Log
.Error("RFB failure: " + details
);
686 this._rfb_clean_disconnect
= false; //This is sent to the UI
688 // Transition to disconnected without waiting for socket to close
689 this._updateConnectionState('disconnecting');
690 this._updateConnectionState('disconnected');
695 _setCapability(cap
, val
) {
696 this._capabilities
[cap
] = val
;
697 this.dispatchEvent(new CustomEvent("capabilities",
698 { detail
: { capabilities
: this._capabilities
} }));
702 if (this._sock
.rQlen
=== 0) {
703 Log
.Warn("handle_message called on an empty receive queue");
707 switch (this._rfb_connection_state
) {
709 Log
.Error("Got data while disconnected");
713 if (this._flushing
) {
716 if (!this._normal_msg()) {
719 if (this._sock
.rQlen
=== 0) {
730 _handleKeyEvent(keysym
, code
, down
) {
731 this.sendKey(keysym
, code
, down
);
734 _handleMouseButton(x
, y
, down
, bmask
) {
736 this._mouse_buttonMask
|= bmask
;
738 this._mouse_buttonMask
&= ~bmask
;
741 if (this.dragViewport
) {
742 if (down
&& !this._viewportDragging
) {
743 this._viewportDragging
= true;
744 this._viewportDragPos
= {'x': x
, 'y': y
};
745 this._viewportHasMoved
= false;
747 // Skip sending mouse events
750 this._viewportDragging
= false;
752 // If we actually performed a drag then we are done
753 // here and should not send any mouse events
754 if (this._viewportHasMoved
) {
758 // Otherwise we treat this as a mouse click event.
759 // Send the button down event here, as the button up
760 // event is sent at the end of this function.
761 RFB
.messages
.pointerEvent(this._sock
,
762 this._display
.absX(x
),
763 this._display
.absY(y
),
768 if (this._viewOnly
) { return; } // View only, skip mouse events
770 if (this._rfb_connection_state
!== 'connected') { return; }
771 RFB
.messages
.pointerEvent(this._sock
, this._display
.absX(x
), this._display
.absY(y
), this._mouse_buttonMask
);
774 _handleMouseMove(x
, y
) {
775 if (this._viewportDragging
) {
776 const deltaX
= this._viewportDragPos
.x
- x
;
777 const deltaY
= this._viewportDragPos
.y
- y
;
779 if (this._viewportHasMoved
|| (Math
.abs(deltaX
) > dragThreshold
||
780 Math
.abs(deltaY
) > dragThreshold
)) {
781 this._viewportHasMoved
= true;
783 this._viewportDragPos
= {'x': x
, 'y': y
};
784 this._display
.viewportChangePos(deltaX
, deltaY
);
787 // Skip sending mouse events
791 if (this._viewOnly
) { return; } // View only, skip mouse events
793 if (this._rfb_connection_state
!== 'connected') { return; }
794 RFB
.messages
.pointerEvent(this._sock
, this._display
.absX(x
), this._display
.absY(y
), this._mouse_buttonMask
);
799 _negotiate_protocol_version() {
800 if (this._sock
.rQwait("version", 12)) {
804 const sversion
= this._sock
.rQshiftStr(12).substr(4, 7);
805 Log
.Info("Server ProtocolVersion: " + sversion
);
808 case "000.000": // UltraVNC repeater
812 case "003.006": // UltraVNC
813 case "003.889": // Apple Remote Desktop
814 this._rfb_version
= 3.3;
817 this._rfb_version
= 3.7;
820 case "004.000": // Intel AMT KVM
821 case "004.001": // RealVNC 4.6
822 case "005.000": // RealVNC 5.3
823 this._rfb_version
= 3.8;
826 return this._fail("Invalid server version " + sversion
);
830 let repeaterID
= "ID:" + this._repeaterID
;
831 while (repeaterID
.length
< 250) {
834 this._sock
.send_string(repeaterID
);
838 if (this._rfb_version
> this._rfb_max_version
) {
839 this._rfb_version
= this._rfb_max_version
;
842 const cversion
= "00" + parseInt(this._rfb_version
, 10) +
843 ".00" + ((this._rfb_version
* 10) % 10);
844 this._sock
.send_string("RFB " + cversion
+ "\n");
845 Log
.Debug('Sent ProtocolVersion: ' + cversion
);
847 this._rfb_init_state
= 'Security';
850 _negotiate_security() {
851 // Polyfill since IE and PhantomJS doesn't have
852 // TypedArray.includes()
853 function includes(item
, array
) {
854 for (let i
= 0; i
< array
.length
; i
++) {
855 if (array
[i
] === item
) {
862 if (this._rfb_version
>= 3.7) {
863 // Server sends supported list, client decides
864 const num_types
= this._sock
.rQshift8();
865 if (this._sock
.rQwait("security type", num_types
, 1)) { return false; }
867 if (num_types
=== 0) {
868 this._rfb_init_state
= "SecurityReason";
869 this._security_context
= "no security types";
870 this._security_status
= 1;
871 return this._init_msg();
874 const types
= this._sock
.rQshiftBytes(num_types
);
875 Log
.Debug("Server security types: " + types
);
877 // Look for each auth in preferred order
878 if (includes(1, types
)) {
879 this._rfb_auth_scheme
= 1; // None
880 } else if (includes(22, types
)) {
881 this._rfb_auth_scheme
= 22; // XVP
882 } else if (includes(16, types
)) {
883 this._rfb_auth_scheme
= 16; // Tight
884 } else if (includes(2, types
)) {
885 this._rfb_auth_scheme
= 2; // VNC Auth
887 return this._fail("Unsupported security types (types: " + types
+ ")");
890 this._sock
.send([this._rfb_auth_scheme
]);
893 if (this._sock
.rQwait("security scheme", 4)) { return false; }
894 this._rfb_auth_scheme
= this._sock
.rQshift32();
896 if (this._rfb_auth_scheme
== 0) {
897 this._rfb_init_state
= "SecurityReason";
898 this._security_context
= "authentication scheme";
899 this._security_status
= 1;
900 return this._init_msg();
904 this._rfb_init_state
= 'Authentication';
905 Log
.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme
);
907 return this._init_msg(); // jump to authentication
910 _handle_security_reason() {
911 if (this._sock
.rQwait("reason length", 4)) {
914 const strlen
= this._sock
.rQshift32();
918 if (this._sock
.rQwait("reason", strlen
, 4)) { return false; }
919 reason
= this._sock
.rQshiftStr(strlen
);
923 this.dispatchEvent(new CustomEvent(
925 { detail
: { status
: this._security_status
,
926 reason
: reason
} }));
928 return this._fail("Security negotiation failed on " +
929 this._security_context
+
930 " (reason: " + reason
+ ")");
932 this.dispatchEvent(new CustomEvent(
934 { detail
: { status
: this._security_status
} }));
936 return this._fail("Security negotiation failed on " +
937 this._security_context
);
942 _negotiate_xvp_auth() {
943 if (this._rfb_credentials
.username
=== undefined ||
944 this._rfb_credentials
.password
=== undefined ||
945 this._rfb_credentials
.target
=== undefined) {
946 this.dispatchEvent(new CustomEvent(
947 "credentialsrequired",
948 { detail
: { types
: ["username", "password", "target"] } }));
952 const xvp_auth_str
= String
.fromCharCode(this._rfb_credentials
.username
.length
) +
953 String
.fromCharCode(this._rfb_credentials
.target
.length
) +
954 this._rfb_credentials
.username
+
955 this._rfb_credentials
.target
;
956 this._sock
.send_string(xvp_auth_str
);
957 this._rfb_auth_scheme
= 2;
958 return this._negotiate_authentication();
961 _negotiate_std_vnc_auth() {
962 if (this._sock
.rQwait("auth challenge", 16)) { return false; }
964 if (this._rfb_credentials
.password
=== undefined) {
965 this.dispatchEvent(new CustomEvent(
966 "credentialsrequired",
967 { detail
: { types
: ["password"] } }));
971 // TODO(directxman12): make genDES not require an Array
972 const challenge
= Array
.prototype.slice
.call(this._sock
.rQshiftBytes(16));
973 const response
= RFB
.genDES(this._rfb_credentials
.password
, challenge
);
974 this._sock
.send(response
);
975 this._rfb_init_state
= "SecurityResult";
979 _negotiate_tight_unix_auth() {
980 if (this._rfb_credentials
.username
=== undefined ||
981 this._rfb_credentials
.password
=== undefined) {
982 this.dispatchEvent(new CustomEvent(
983 "credentialsrequired",
984 { detail
: { types
: ["username", "password"] } }));
988 this._sock
.send([0, 0, 0, this._rfb_credentials
.username
.length
]);
989 this._sock
.send([0, 0, 0, this._rfb_credentials
.password
.length
]);
990 this._sock
.send_string(this._rfb_credentials
.username
);
991 this._sock
.send_string(this._rfb_credentials
.password
);
992 this._rfb_init_state
= "SecurityResult";
996 _negotiate_tight_tunnels(numTunnels
) {
997 const clientSupportedTunnelTypes
= {
998 0: { vendor
: 'TGHT', signature
: 'NOTUNNEL' }
1000 const serverSupportedTunnelTypes
= {};
1001 // receive tunnel capabilities
1002 for (let i
= 0; i
< numTunnels
; i
++) {
1003 const cap_code
= this._sock
.rQshift32();
1004 const cap_vendor
= this._sock
.rQshiftStr(4);
1005 const cap_signature
= this._sock
.rQshiftStr(8);
1006 serverSupportedTunnelTypes
[cap_code
] = { vendor
: cap_vendor
, signature
: cap_signature
};
1009 Log
.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes
);
1011 // Siemens touch panels have a VNC server that supports NOTUNNEL,
1012 // but forgets to advertise it. Try to detect such servers by
1013 // looking for their custom tunnel type.
1014 if (serverSupportedTunnelTypes
[1] &&
1015 (serverSupportedTunnelTypes
[1].vendor
=== "SICR") &&
1016 (serverSupportedTunnelTypes
[1].signature
=== "SCHANNEL")) {
1017 Log
.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
1018 serverSupportedTunnelTypes
[0] = { vendor
: 'TGHT', signature
: 'NOTUNNEL' };
1021 // choose the notunnel type
1022 if (serverSupportedTunnelTypes
[0]) {
1023 if (serverSupportedTunnelTypes
[0].vendor
!= clientSupportedTunnelTypes
[0].vendor
||
1024 serverSupportedTunnelTypes
[0].signature
!= clientSupportedTunnelTypes
[0].signature
) {
1025 return this._fail("Client's tunnel type had the incorrect " +
1026 "vendor or signature");
1028 Log
.Debug("Selected tunnel type: " + clientSupportedTunnelTypes
[0]);
1029 this._sock
.send([0, 0, 0, 0]); // use NOTUNNEL
1030 return false; // wait until we receive the sub auth count to continue
1032 return this._fail("Server wanted tunnels, but doesn't support " +
1033 "the notunnel type");
1037 _negotiate_tight_auth() {
1038 if (!this._rfb_tightvnc
) { // first pass, do the tunnel negotiation
1039 if (this._sock
.rQwait("num tunnels", 4)) { return false; }
1040 const numTunnels
= this._sock
.rQshift32();
1041 if (numTunnels
> 0 && this._sock
.rQwait("tunnel capabilities", 16 * numTunnels
, 4)) { return false; }
1043 this._rfb_tightvnc
= true;
1045 if (numTunnels
> 0) {
1046 this._negotiate_tight_tunnels(numTunnels
);
1047 return false; // wait until we receive the sub auth to continue
1051 // second pass, do the sub-auth negotiation
1052 if (this._sock
.rQwait("sub auth count", 4)) { return false; }
1053 const subAuthCount
= this._sock
.rQshift32();
1054 if (subAuthCount
=== 0) { // empty sub-auth list received means 'no auth' subtype selected
1055 this._rfb_init_state
= 'SecurityResult';
1059 if (this._sock
.rQwait("sub auth capabilities", 16 * subAuthCount
, 4)) { return false; }
1061 const clientSupportedTypes
= {
1067 const serverSupportedTypes
= [];
1069 for (let i
= 0; i
< subAuthCount
; i
++) {
1070 this._sock
.rQshift32(); // capNum
1071 const capabilities
= this._sock
.rQshiftStr(12);
1072 serverSupportedTypes
.push(capabilities
);
1075 Log
.Debug("Server Tight authentication types: " + serverSupportedTypes
);
1077 for (let authType
in clientSupportedTypes
) {
1078 if (serverSupportedTypes
.indexOf(authType
) != -1) {
1079 this._sock
.send([0, 0, 0, clientSupportedTypes
[authType
]]);
1080 Log
.Debug("Selected authentication type: " + authType
);
1083 case 'STDVNOAUTH__': // no auth
1084 this._rfb_init_state
= 'SecurityResult';
1086 case 'STDVVNCAUTH_': // VNC auth
1087 this._rfb_auth_scheme
= 2;
1088 return this._init_msg();
1089 case 'TGHTULGNAUTH': // UNIX auth
1090 this._rfb_auth_scheme
= 129;
1091 return this._init_msg();
1093 return this._fail("Unsupported tiny auth scheme " +
1094 "(scheme: " + authType
+ ")");
1099 return this._fail("No supported sub-auth types!");
1102 _negotiate_authentication() {
1103 switch (this._rfb_auth_scheme
) {
1105 if (this._rfb_version
>= 3.8) {
1106 this._rfb_init_state
= 'SecurityResult';
1109 this._rfb_init_state
= 'ClientInitialisation';
1110 return this._init_msg();
1112 case 22: // XVP auth
1113 return this._negotiate_xvp_auth();
1115 case 2: // VNC authentication
1116 return this._negotiate_std_vnc_auth();
1118 case 16: // TightVNC Security Type
1119 return this._negotiate_tight_auth();
1121 case 129: // TightVNC UNIX Security Type
1122 return this._negotiate_tight_unix_auth();
1125 return this._fail("Unsupported auth scheme (scheme: " +
1126 this._rfb_auth_scheme
+ ")");
1130 _handle_security_result() {
1131 if (this._sock
.rQwait('VNC auth response ', 4)) { return false; }
1133 const status
= this._sock
.rQshift32();
1135 if (status
=== 0) { // OK
1136 this._rfb_init_state
= 'ClientInitialisation';
1137 Log
.Debug('Authentication OK');
1138 return this._init_msg();
1140 if (this._rfb_version
>= 3.8) {
1141 this._rfb_init_state
= "SecurityReason";
1142 this._security_context
= "security result";
1143 this._security_status
= status
;
1144 return this._init_msg();
1146 this.dispatchEvent(new CustomEvent(
1148 { detail
: { status
: status
} }));
1150 return this._fail("Security handshake failed");
1155 _negotiate_server_init() {
1156 if (this._sock
.rQwait("server initialization", 24)) { return false; }
1159 const width
= this._sock
.rQshift16();
1160 const height
= this._sock
.rQshift16();
1163 const bpp
= this._sock
.rQshift8();
1164 const depth
= this._sock
.rQshift8();
1165 const big_endian
= this._sock
.rQshift8();
1166 const true_color
= this._sock
.rQshift8();
1168 const red_max
= this._sock
.rQshift16();
1169 const green_max
= this._sock
.rQshift16();
1170 const blue_max
= this._sock
.rQshift16();
1171 const red_shift
= this._sock
.rQshift8();
1172 const green_shift
= this._sock
.rQshift8();
1173 const blue_shift
= this._sock
.rQshift8();
1174 this._sock
.rQskipBytes(3); // padding
1176 // NB(directxman12): we don't want to call any callbacks or print messages until
1177 // *after* we're past the point where we could backtrack
1179 /* Connection name/title */
1180 const name_length = this._sock.rQshift32();
1181 if (this._sock.rQwait('server init name', name_length, 24)) { return false; }
1182 let name = this._sock.rQshiftStr(name_length);
1184 name = decodeUTF8(name);
1189 if (this._rfb_tightvnc) {
1190 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; }
1191 // In TightVNC mode, ServerInit message is extended
1192 const numServerMessages = this._sock.rQshift16();
1193 const numClientMessages = this._sock.rQshift16();
1194 const numEncodings = this._sock.rQshift16();
1195 this._sock.rQskipBytes(2); // padding
1197 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
1198 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; }
1200 // we don't actually do anything with the capability information that TIGHT sends,
1201 // so we just skip the all of this.
1203 // TIGHT server message capabilities
1204 this._sock.rQskipBytes(16 * numServerMessages);
1206 // TIGHT client message capabilities
1207 this._sock.rQskipBytes(16 * numClientMessages);
1209 // TIGHT encoding capabilities
1210 this._sock.rQskipBytes(16 * numEncodings);
1213 // NB(directxman12): these are down here so that we don't run them multiple times
1215 Log.Info("Screen: " + width + "x" + height +
1216 ", bpp: " + bpp + ", depth: " + depth +
1217 ", big_endian: " + big_endian +
1218 ", true_color: " + true_color +
1219 ", red_max: " + red_max +
1220 ", green_max: " + green_max +
1221 ", blue_max: " + blue_max +
1222 ", red_shift: " + red_shift +
1223 ", green_shift: " + green_shift +
1224 ", blue_shift: " + blue_shift);
1226 // we're past the point where we could backtrack, so it's safe to call this
1227 this._setDesktopName(name);
1228 this._resize(width, height);
1230 if (!this._viewOnly) { this._keyboard.grab(); }
1231 if (!this._viewOnly) { this._mouse.grab(); }
1233 this._fb_depth = 24;
1235 if (this._fb_name === "Intel(r) AMT KVM") {
1236 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
1240 RFB.messages.pixelFormat(this._sock, this._fb_depth, true);
1241 this._sendEncodings();
1242 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height);
1244 this._updateConnectionState('connected');
1251 // In preference order
1252 encs.push(encodings.encodingCopyRect);
1253 // Only supported with full depth support
1254 if (this._fb_depth == 24) {
1255 encs.push(encodings.encodingTight);
1256 encs.push(encodings.encodingTightPNG);
1257 encs.push(encodings.encodingHextile);
1258 encs.push(encodings.encodingRRE);
1260 encs.push(encodings.encodingRaw);
1262 // Psuedo-encoding settings
1263 encs.push(encodings.pseudoEncodingQualityLevel0 + 6);
1264 encs.push(encodings.pseudoEncodingCompressLevel0 + 2);
1266 encs.push(encodings.pseudoEncodingDesktopSize);
1267 encs.push(encodings.pseudoEncodingLastRect);
1268 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
1269 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
1270 encs.push(encodings.pseudoEncodingXvp);
1271 encs.push(encodings.pseudoEncodingFence);
1272 encs.push(encodings.pseudoEncodingContinuousUpdates);
1273 encs.push(encodings.pseudoEncodingDesktopName);
1275 if (this._fb_depth == 24) {
1276 encs.push(encodings.pseudoEncodingVMwareCursor);
1277 encs.push(encodings.pseudoEncodingCursor);
1280 RFB.messages.clientEncodings(this._sock, encs);
1283 /* RFB protocol initialization states:
1288 * ClientInitialization - not triggered by server message
1289 * ServerInitialization
1292 switch (this._rfb_init_state
) {
1293 case 'ProtocolVersion':
1294 return this._negotiate_protocol_version();
1297 return this._negotiate_security();
1299 case 'Authentication':
1300 return this._negotiate_authentication();
1302 case 'SecurityResult':
1303 return this._handle_security_result();
1305 case 'SecurityReason':
1306 return this._handle_security_reason();
1308 case 'ClientInitialisation':
1309 this._sock
.send([this._shared
? 1 : 0]); // ClientInitialisation
1310 this._rfb_init_state
= 'ServerInitialisation';
1313 case 'ServerInitialisation':
1314 return this._negotiate_server_init();
1317 return this._fail("Unknown init state (state: " +
1318 this._rfb_init_state
+ ")");
1322 _handle_set_colour_map_msg() {
1323 Log
.Debug("SetColorMapEntries");
1325 return this._fail("Unexpected SetColorMapEntries message");
1328 _handle_server_cut_text() {
1329 Log
.Debug("ServerCutText");
1331 if (this._sock
.rQwait("ServerCutText header", 7, 1)) { return false; }
1332 this._sock
.rQskipBytes(3); // Padding
1333 const length
= this._sock
.rQshift32();
1334 if (this._sock
.rQwait("ServerCutText", length
, 8)) { return false; }
1336 const text
= this._sock
.rQshiftStr(length
);
1338 if (this._viewOnly
) { return true; }
1340 this.dispatchEvent(new CustomEvent(
1342 { detail
: { text
: text
} }));
1347 _handle_server_fence_msg() {
1348 if (this._sock
.rQwait("ServerFence header", 8, 1)) { return false; }
1349 this._sock
.rQskipBytes(3); // Padding
1350 let flags
= this._sock
.rQshift32();
1351 let length
= this._sock
.rQshift8();
1353 if (this._sock
.rQwait("ServerFence payload", length
, 9)) { return false; }
1356 Log
.Warn("Bad payload length (" + length
+ ") in fence response");
1360 const payload
= this._sock
.rQshiftStr(length
);
1362 this._supportsFence
= true;
1367 * (1<<0) - BlockBefore
1368 * (1<<1) - BlockAfter
1373 if (!(flags
& (1<<31))) {
1374 return this._fail("Unexpected fence response");
1377 // Filter out unsupported flags
1378 // FIXME: support syncNext
1379 flags
&= (1<<0) | (1<<1);
1381 // BlockBefore and BlockAfter are automatically handled by
1382 // the fact that we process each incoming message
1384 RFB
.messages
.clientFence(this._sock
, flags
, payload
);
1390 if (this._sock
.rQwait("XVP version and message", 3, 1)) { return false; }
1391 this._sock
.rQskipBytes(1); // Padding
1392 const xvp_ver
= this._sock
.rQshift8();
1393 const xvp_msg
= this._sock
.rQshift8();
1397 Log
.Error("XVP Operation Failed");
1400 this._rfb_xvp_ver
= xvp_ver
;
1401 Log
.Info("XVP extensions enabled (version " + this._rfb_xvp_ver
+ ")");
1402 this._setCapability("power", true);
1405 this._fail("Illegal server XVP message (msg: " + xvp_msg
+ ")");
1414 if (this._FBU
.rects
> 0) {
1417 msg_type
= this._sock
.rQshift8();
1422 case 0: // FramebufferUpdate
1423 ret
= this._framebufferUpdate();
1424 if (ret
&& !this._enabledContinuousUpdates
) {
1425 RFB
.messages
.fbUpdateRequest(this._sock
, true, 0, 0,
1426 this._fb_width
, this._fb_height
);
1430 case 1: // SetColorMapEntries
1431 return this._handle_set_colour_map_msg();
1435 this.dispatchEvent(new CustomEvent(
1440 case 3: // ServerCutText
1441 return this._handle_server_cut_text();
1443 case 150: // EndOfContinuousUpdates
1444 first
= !this._supportsContinuousUpdates
;
1445 this._supportsContinuousUpdates
= true;
1446 this._enabledContinuousUpdates
= false;
1448 this._enabledContinuousUpdates
= true;
1449 this._updateContinuousUpdates();
1450 Log
.Info("Enabling continuous updates.");
1452 // FIXME: We need to send a framebufferupdaterequest here
1453 // if we add support for turning off continuous updates
1457 case 248: // ServerFence
1458 return this._handle_server_fence_msg();
1461 return this._handle_xvp_msg();
1464 this._fail("Unexpected server message (type " + msg_type
+ ")");
1465 Log
.Debug("sock.rQslice(0, 30): " + this._sock
.rQslice(0, 30));
1471 this._flushing
= false;
1472 // Resume processing
1473 if (this._sock
.rQlen
> 0) {
1474 this._handle_message();
1478 _framebufferUpdate() {
1479 if (this._FBU
.rects
=== 0) {
1480 if (this._sock
.rQwait("FBU header", 3, 1)) { return false; }
1481 this._sock
.rQskipBytes(1); // Padding
1482 this._FBU
.rects
= this._sock
.rQshift16();
1484 // Make sure the previous frame is fully rendered first
1485 // to avoid building up an excessive queue
1486 if (this._display
.pending()) {
1487 this._flushing
= true;
1488 this._display
.flush();
1493 while (this._FBU
.rects
> 0) {
1494 if (this._FBU
.encoding
=== null) {
1495 if (this._sock
.rQwait("rect header", 12)) { return false; }
1496 /* New FramebufferUpdate */
1498 const hdr
= this._sock
.rQshiftBytes(12);
1499 this._FBU
.x
= (hdr
[0] << 8) + hdr
[1];
1500 this._FBU
.y
= (hdr
[2] << 8) + hdr
[3];
1501 this._FBU
.width
= (hdr
[4] << 8) + hdr
[5];
1502 this._FBU
.height
= (hdr
[6] << 8) + hdr
[7];
1503 this._FBU
.encoding
= parseInt((hdr
[8] << 24) + (hdr
[9] << 16) +
1504 (hdr
[10] << 8) + hdr
[11], 10);
1507 if (!this._handleRect()) {
1512 this._FBU
.encoding
= null;
1515 this._display
.flip();
1517 return true; // We finished this FBU
1521 switch (this._FBU
.encoding
) {
1522 case encodings
.pseudoEncodingLastRect
:
1523 this._FBU
.rects
= 1; // Will be decreased when we return
1526 case encodings
.pseudoEncodingVMwareCursor
:
1527 return this._handleVMwareCursor();
1529 case encodings
.pseudoEncodingCursor
:
1530 return this._handleCursor();
1532 case encodings
.pseudoEncodingQEMUExtendedKeyEvent
:
1533 // Old Safari doesn't support creating keyboard events
1535 const keyboardEvent
= document
.createEvent("keyboardEvent");
1536 if (keyboardEvent
.code
!== undefined) {
1537 this._qemuExtKeyEventSupported
= true;
1544 case encodings
.pseudoEncodingDesktopName
:
1545 return this._handleDesktopName();
1547 case encodings
.pseudoEncodingDesktopSize
:
1548 this._resize(this._FBU
.width
, this._FBU
.height
);
1551 case encodings
.pseudoEncodingExtendedDesktopSize
:
1552 return this._handleExtendedDesktopSize();
1555 return this._handleDataRect();
1559 _handleVMwareCursor() {
1560 const hotx
= this._FBU
.x
; // hotspot-x
1561 const hoty
= this._FBU
.y
; // hotspot-y
1562 const w
= this._FBU
.width
;
1563 const h
= this._FBU
.height
;
1564 if (this._sock
.rQwait("VMware cursor encoding", 1)) {
1568 const cursor_type
= this._sock
.rQshift8();
1570 this._sock
.rQshift8(); //Padding
1573 const bytesPerPixel
= 4;
1576 if (cursor_type
== 0) {
1577 //Used to filter away unimportant bits.
1578 //OR is used for correct conversion in js.
1579 const PIXEL_MASK
= 0xffffff00 | 0;
1580 rgba
= new Array(w
* h
* bytesPerPixel
);
1582 if (this._sock
.rQwait("VMware cursor classic encoding",
1583 (w
* h
* bytesPerPixel
) * 2, 2)) {
1587 let and_mask
= new Array(w
* h
);
1588 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
1589 and_mask
[pixel
] = this._sock
.rQshift32();
1592 let xor_mask
= new Array(w
* h
);
1593 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
1594 xor_mask
[pixel
] = this._sock
.rQshift32();
1597 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
1598 if (and_mask
[pixel
] == 0) {
1599 //Fully opaque pixel
1600 let bgr
= xor_mask
[pixel
];
1601 let r
= bgr
>> 8 & 0xff;
1602 let g
= bgr
>> 16 & 0xff;
1603 let b
= bgr
>> 24 & 0xff;
1605 rgba
[(pixel
* bytesPerPixel
) ] = r
; //r
1606 rgba
[(pixel
* bytesPerPixel
) + 1 ] = g
; //g
1607 rgba
[(pixel
* bytesPerPixel
) + 2 ] = b
; //b
1608 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff; //a
1610 } else if ((and_mask
[pixel
] & PIXEL_MASK
) ==
1612 //Only screen value matters, no mouse colouring
1613 if (xor_mask
[pixel
] == 0) {
1615 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
1616 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
1617 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
1618 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0x00;
1620 } else if ((xor_mask
[pixel
] & PIXEL_MASK
) ==
1622 //Inverted pixel, not supported in browsers.
1623 //Fully opaque instead.
1624 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
1625 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
1626 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
1627 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
1630 //Unhandled xor_mask
1631 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
1632 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
1633 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
1634 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
1638 //Unhandled and_mask
1639 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
1640 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
1641 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
1642 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
1647 } else if (cursor_type
== 1) {
1648 if (this._sock
.rQwait("VMware cursor alpha encoding",
1653 rgba
= new Array(w
* h
* bytesPerPixel
);
1655 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
1656 let data
= this._sock
.rQshift32();
1658 rgba
[(pixel
* 4) ] = data
>> 8 & 0xff; //r
1659 rgba
[(pixel
* 4) + 1 ] = data
>> 16 & 0xff; //g
1660 rgba
[(pixel
* 4) + 2 ] = data
>> 24 & 0xff; //b
1661 rgba
[(pixel
* 4) + 3 ] = data
& 0xff; //a
1665 Log
.Warn("The given cursor type is not supported: "
1666 + cursor_type
+ " given.");
1670 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
1676 const hotx
= this._FBU
.x
; // hotspot-x
1677 const hoty
= this._FBU
.y
; // hotspot-y
1678 const w
= this._FBU
.width
;
1679 const h
= this._FBU
.height
;
1681 const pixelslength
= w
* h
* 4;
1682 const masklength
= Math
.ceil(w
/ 8) * h
;
1684 let bytes
= pixelslength
+ masklength
;
1685 if (this._sock
.rQwait("cursor encoding", bytes
)) {
1689 // Decode from BGRX pixels + bit mask to RGBA
1690 const pixels
= this._sock
.rQshiftBytes(pixelslength
);
1691 const mask
= this._sock
.rQshiftBytes(masklength
);
1692 let rgba
= new Uint8Array(w
* h
* 4);
1695 for (let y
= 0; y
< h
; y
++) {
1696 for (let x
= 0; x
< w
; x
++) {
1697 let mask_idx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/ 8);
1698 let alpha
= (mask
[mask_idx
] << (x
% 8)) & 0x80 ? 255 : 0;
1699 rgba
[pix_idx
] = pixels
[pix_idx
+ 2];
1700 rgba
[pix_idx
+ 1] = pixels
[pix_idx
+ 1];
1701 rgba
[pix_idx
+ 2] = pixels
[pix_idx
];
1702 rgba
[pix_idx
+ 3] = alpha
;
1707 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
1712 _handleDesktopName() {
1713 if (this._sock
.rQwait("DesktopName", 4)) {
1717 let length
= this._sock
.rQshift32();
1719 if (this._sock
.rQwait("DesktopName", length
, 4)) {
1723 let name
= this._sock
.rQshiftStr(length
);
1725 name
= decodeUTF8(name
);
1730 this._setDesktopName(name
);
1735 _handleExtendedDesktopSize() {
1736 if (this._sock
.rQwait("ExtendedDesktopSize", 4)) {
1740 const number_of_screens
= this._sock
.rQpeek8();
1742 let bytes
= 4 + (number_of_screens
* 16);
1743 if (this._sock
.rQwait("ExtendedDesktopSize", bytes
)) {
1747 const firstUpdate
= !this._supportsSetDesktopSize
;
1748 this._supportsSetDesktopSize
= true;
1750 // Normally we only apply the current resize mode after a
1751 // window resize event. However there is no such trigger on the
1752 // initial connect. And we don't know if the server supports
1753 // resizing until we've gotten here.
1755 this._requestRemoteResize();
1758 this._sock
.rQskipBytes(1); // number-of-screens
1759 this._sock
.rQskipBytes(3); // padding
1761 for (let i
= 0; i
< number_of_screens
; i
+= 1) {
1762 // Save the id and flags of the first screen
1764 this._screen_id
= this._sock
.rQshiftBytes(4); // id
1765 this._sock
.rQskipBytes(2); // x-position
1766 this._sock
.rQskipBytes(2); // y-position
1767 this._sock
.rQskipBytes(2); // width
1768 this._sock
.rQskipBytes(2); // height
1769 this._screen_flags
= this._sock
.rQshiftBytes(4); // flags
1771 this._sock
.rQskipBytes(16);
1776 * The x-position indicates the reason for the change:
1778 * 0 - server resized on its own
1779 * 1 - this client requested the resize
1780 * 2 - another client requested the resize
1783 // We need to handle errors when we requested the resize.
1784 if (this._FBU
.x
=== 1 && this._FBU
.y
!== 0) {
1786 // The y-position indicates the status code from the server
1787 switch (this._FBU
.y
) {
1789 msg
= "Resize is administratively prohibited";
1792 msg
= "Out of resources";
1795 msg
= "Invalid screen layout";
1798 msg
= "Unknown reason";
1801 Log
.Warn("Server did not accept the resize request: "
1804 this._resize(this._FBU
.width
, this._FBU
.height
);
1811 let decoder
= this._decoders
[this._FBU
.encoding
];
1813 this._fail("Unsupported encoding (encoding: " +
1814 this._FBU
.encoding
+ ")");
1819 return decoder
.decodeRect(this._FBU
.x
, this._FBU
.y
,
1820 this._FBU
.width
, this._FBU
.height
,
1821 this._sock
, this._display
,
1824 this._fail("Error decoding rect: " + err
);
1829 _updateContinuousUpdates() {
1830 if (!this._enabledContinuousUpdates
) { return; }
1832 RFB
.messages
.enableContinuousUpdates(this._sock
, true, 0, 0,
1833 this._fb_width
, this._fb_height
);
1836 _resize(width
, height
) {
1837 this._fb_width
= width
;
1838 this._fb_height
= height
;
1840 this._display
.resize(this._fb_width
, this._fb_height
);
1842 // Adjust the visible viewport based on the new dimensions
1844 this._updateScale();
1846 this._updateContinuousUpdates();
1850 if (this._rfb_xvp_ver
< ver
) { return; }
1851 Log
.Info("Sending XVP operation " + op
+ " (version " + ver
+ ")");
1852 RFB
.messages
.xvpOp(this._sock
, ver
, op
);
1855 _updateCursor(rgba
, hotx
, hoty
, w
, h
) {
1856 this._cursorImage
= {
1858 hotx
: hotx
, hoty
: hoty
, w
: w
, h
: h
,
1860 this._refreshCursor();
1863 _shouldShowDotCursor() {
1864 // Called when this._cursorImage is updated
1865 if (!this._showDotCursor
) {
1866 // User does not want to see the dot, so...
1870 // The dot should not be shown if the cursor is already visible,
1871 // i.e. contains at least one not-fully-transparent pixel.
1872 // So iterate through all alpha bytes in rgba and stop at the
1874 for (let i
= 3; i
< this._cursorImage
.rgbaPixels
.length
; i
+= 4) {
1875 if (this._cursorImage
.rgbaPixels
[i
]) {
1880 // At this point, we know that the cursor is fully transparent, and
1881 // the user wants to see the dot instead of this.
1886 if (this._rfb_connection_state
!== 'connected') { return; }
1887 const image
= this._shouldShowDotCursor() ? RFB
.cursors
.dot
: this._cursorImage
;
1888 this._cursor
.change(image
.rgbaPixels
,
1889 image
.hotx
, image
.hoty
,
1894 static genDES(password
, challenge
) {
1895 const passwordChars
= password
.split('').map(c
=> c
.charCodeAt(0));
1896 return (new DES(passwordChars
)).encrypt(challenge
);
1902 keyEvent(sock
, keysym
, down
) {
1903 const buff
= sock
._sQ
;
1904 const offset
= sock
._sQlen
;
1906 buff
[offset
] = 4; // msg-type
1907 buff
[offset
+ 1] = down
;
1909 buff
[offset
+ 2] = 0;
1910 buff
[offset
+ 3] = 0;
1912 buff
[offset
+ 4] = (keysym
>> 24);
1913 buff
[offset
+ 5] = (keysym
>> 16);
1914 buff
[offset
+ 6] = (keysym
>> 8);
1915 buff
[offset
+ 7] = keysym
;
1921 QEMUExtendedKeyEvent(sock
, keysym
, down
, keycode
) {
1922 function getRFBkeycode(xt_scancode
) {
1923 const upperByte
= (keycode
>> 8);
1924 const lowerByte
= (keycode
& 0x00ff);
1925 if (upperByte
=== 0xe0 && lowerByte
< 0x7f) {
1926 return lowerByte
| 0x80;
1931 const buff
= sock
._sQ
;
1932 const offset
= sock
._sQlen
;
1934 buff
[offset
] = 255; // msg-type
1935 buff
[offset
+ 1] = 0; // sub msg-type
1937 buff
[offset
+ 2] = (down
>> 8);
1938 buff
[offset
+ 3] = down
;
1940 buff
[offset
+ 4] = (keysym
>> 24);
1941 buff
[offset
+ 5] = (keysym
>> 16);
1942 buff
[offset
+ 6] = (keysym
>> 8);
1943 buff
[offset
+ 7] = keysym
;
1945 const RFBkeycode
= getRFBkeycode(keycode
);
1947 buff
[offset
+ 8] = (RFBkeycode
>> 24);
1948 buff
[offset
+ 9] = (RFBkeycode
>> 16);
1949 buff
[offset
+ 10] = (RFBkeycode
>> 8);
1950 buff
[offset
+ 11] = RFBkeycode
;
1956 pointerEvent(sock
, x
, y
, mask
) {
1957 const buff
= sock
._sQ
;
1958 const offset
= sock
._sQlen
;
1960 buff
[offset
] = 5; // msg-type
1962 buff
[offset
+ 1] = mask
;
1964 buff
[offset
+ 2] = x
>> 8;
1965 buff
[offset
+ 3] = x
;
1967 buff
[offset
+ 4] = y
>> 8;
1968 buff
[offset
+ 5] = y
;
1974 // TODO(directxman12): make this unicode compatible?
1975 clientCutText(sock
, text
) {
1976 const buff
= sock
._sQ
;
1977 const offset
= sock
._sQlen
;
1979 buff
[offset
] = 6; // msg-type
1981 buff
[offset
+ 1] = 0; // padding
1982 buff
[offset
+ 2] = 0; // padding
1983 buff
[offset
+ 3] = 0; // padding
1985 let length
= text
.length
;
1987 buff
[offset
+ 4] = length
>> 24;
1988 buff
[offset
+ 5] = length
>> 16;
1989 buff
[offset
+ 6] = length
>> 8;
1990 buff
[offset
+ 7] = length
;
1994 // We have to keep track of from where in the text we begin creating the
1995 // buffer for the flush in the next iteration.
1998 let remaining
= length
;
1999 while (remaining
> 0) {
2001 let flushSize
= Math
.min(remaining
, (sock
._sQbufferSize
- sock
._sQlen
));
2002 for (let i
= 0; i
< flushSize
; i
++) {
2003 buff
[sock
._sQlen
+ i
] = text
.charCodeAt(textOffset
+ i
);
2006 sock
._sQlen
+= flushSize
;
2009 remaining
-= flushSize
;
2010 textOffset
+= flushSize
;
2014 setDesktopSize(sock
, width
, height
, id
, flags
) {
2015 const buff
= sock
._sQ
;
2016 const offset
= sock
._sQlen
;
2018 buff
[offset
] = 251; // msg-type
2019 buff
[offset
+ 1] = 0; // padding
2020 buff
[offset
+ 2] = width
>> 8; // width
2021 buff
[offset
+ 3] = width
;
2022 buff
[offset
+ 4] = height
>> 8; // height
2023 buff
[offset
+ 5] = height
;
2025 buff
[offset
+ 6] = 1; // number-of-screens
2026 buff
[offset
+ 7] = 0; // padding
2029 buff
[offset
+ 8] = id
>> 24; // id
2030 buff
[offset
+ 9] = id
>> 16;
2031 buff
[offset
+ 10] = id
>> 8;
2032 buff
[offset
+ 11] = id
;
2033 buff
[offset
+ 12] = 0; // x-position
2034 buff
[offset
+ 13] = 0;
2035 buff
[offset
+ 14] = 0; // y-position
2036 buff
[offset
+ 15] = 0;
2037 buff
[offset
+ 16] = width
>> 8; // width
2038 buff
[offset
+ 17] = width
;
2039 buff
[offset
+ 18] = height
>> 8; // height
2040 buff
[offset
+ 19] = height
;
2041 buff
[offset
+ 20] = flags
>> 24; // flags
2042 buff
[offset
+ 21] = flags
>> 16;
2043 buff
[offset
+ 22] = flags
>> 8;
2044 buff
[offset
+ 23] = flags
;
2050 clientFence(sock
, flags
, payload
) {
2051 const buff
= sock
._sQ
;
2052 const offset
= sock
._sQlen
;
2054 buff
[offset
] = 248; // msg-type
2056 buff
[offset
+ 1] = 0; // padding
2057 buff
[offset
+ 2] = 0; // padding
2058 buff
[offset
+ 3] = 0; // padding
2060 buff
[offset
+ 4] = flags
>> 24; // flags
2061 buff
[offset
+ 5] = flags
>> 16;
2062 buff
[offset
+ 6] = flags
>> 8;
2063 buff
[offset
+ 7] = flags
;
2065 const n
= payload
.length
;
2067 buff
[offset
+ 8] = n
; // length
2069 for (let i
= 0; i
< n
; i
++) {
2070 buff
[offset
+ 9 + i
] = payload
.charCodeAt(i
);
2073 sock
._sQlen
+= 9 + n
;
2077 enableContinuousUpdates(sock
, enable
, x
, y
, width
, height
) {
2078 const buff
= sock
._sQ
;
2079 const offset
= sock
._sQlen
;
2081 buff
[offset
] = 150; // msg-type
2082 buff
[offset
+ 1] = enable
; // enable-flag
2084 buff
[offset
+ 2] = x
>> 8; // x
2085 buff
[offset
+ 3] = x
;
2086 buff
[offset
+ 4] = y
>> 8; // y
2087 buff
[offset
+ 5] = y
;
2088 buff
[offset
+ 6] = width
>> 8; // width
2089 buff
[offset
+ 7] = width
;
2090 buff
[offset
+ 8] = height
>> 8; // height
2091 buff
[offset
+ 9] = height
;
2097 pixelFormat(sock
, depth
, true_color
) {
2098 const buff
= sock
._sQ
;
2099 const offset
= sock
._sQlen
;
2105 } else if (depth
> 8) {
2111 const bits
= Math
.floor(depth
/3);
2113 buff
[offset
] = 0; // msg-type
2115 buff
[offset
+ 1] = 0; // padding
2116 buff
[offset
+ 2] = 0; // padding
2117 buff
[offset
+ 3] = 0; // padding
2119 buff
[offset
+ 4] = bpp
; // bits-per-pixel
2120 buff
[offset
+ 5] = depth
; // depth
2121 buff
[offset
+ 6] = 0; // little-endian
2122 buff
[offset
+ 7] = true_color
? 1 : 0; // true-color
2124 buff
[offset
+ 8] = 0; // red-max
2125 buff
[offset
+ 9] = (1 << bits
) - 1; // red-max
2127 buff
[offset
+ 10] = 0; // green-max
2128 buff
[offset
+ 11] = (1 << bits
) - 1; // green-max
2130 buff
[offset
+ 12] = 0; // blue-max
2131 buff
[offset
+ 13] = (1 << bits
) - 1; // blue-max
2133 buff
[offset
+ 14] = bits
* 2; // red-shift
2134 buff
[offset
+ 15] = bits
* 1; // green-shift
2135 buff
[offset
+ 16] = bits
* 0; // blue-shift
2137 buff
[offset
+ 17] = 0; // padding
2138 buff
[offset
+ 18] = 0; // padding
2139 buff
[offset
+ 19] = 0; // padding
2145 clientEncodings(sock
, encodings
) {
2146 const buff
= sock
._sQ
;
2147 const offset
= sock
._sQlen
;
2149 buff
[offset
] = 2; // msg-type
2150 buff
[offset
+ 1] = 0; // padding
2152 buff
[offset
+ 2] = encodings
.length
>> 8;
2153 buff
[offset
+ 3] = encodings
.length
;
2156 for (let i
= 0; i
< encodings
.length
; i
++) {
2157 const enc
= encodings
[i
];
2158 buff
[j
] = enc
>> 24;
2159 buff
[j
+ 1] = enc
>> 16;
2160 buff
[j
+ 2] = enc
>> 8;
2166 sock
._sQlen
+= j
- offset
;
2170 fbUpdateRequest(sock
, incremental
, x
, y
, w
, h
) {
2171 const buff
= sock
._sQ
;
2172 const offset
= sock
._sQlen
;
2174 if (typeof(x
) === "undefined") { x
= 0; }
2175 if (typeof(y
) === "undefined") { y
= 0; }
2177 buff
[offset
] = 3; // msg-type
2178 buff
[offset
+ 1] = incremental
? 1 : 0;
2180 buff
[offset
+ 2] = (x
>> 8) & 0xFF;
2181 buff
[offset
+ 3] = x
& 0xFF;
2183 buff
[offset
+ 4] = (y
>> 8) & 0xFF;
2184 buff
[offset
+ 5] = y
& 0xFF;
2186 buff
[offset
+ 6] = (w
>> 8) & 0xFF;
2187 buff
[offset
+ 7] = w
& 0xFF;
2189 buff
[offset
+ 8] = (h
>> 8) & 0xFF;
2190 buff
[offset
+ 9] = h
& 0xFF;
2196 xvpOp(sock
, ver
, op
) {
2197 const buff
= sock
._sQ
;
2198 const offset
= sock
._sQlen
;
2200 buff
[offset
] = 250; // msg-type
2201 buff
[offset
+ 1] = 0; // padding
2203 buff
[offset
+ 2] = ver
;
2204 buff
[offset
+ 3] = op
;
2213 rgbaPixels
: new Uint8Array(),
2219 /* eslint-disable indent */
2220 rgbaPixels
: new Uint8Array([
2221 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2222 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
2223 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2225 /* eslint-enable indent */