2 * noVNC: HTML5 VNC client
3 * Copyright (C) 2018 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
||
944 !this._rfb_credentials
.password
||
945 !this._rfb_credentials
.target
) {
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
) {
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_tunnels(numTunnels
) {
980 const clientSupportedTunnelTypes
= {
981 0: { vendor
: 'TGHT', signature
: 'NOTUNNEL' }
983 const serverSupportedTunnelTypes
= {};
984 // receive tunnel capabilities
985 for (let i
= 0; i
< numTunnels
; i
++) {
986 const cap_code
= this._sock
.rQshift32();
987 const cap_vendor
= this._sock
.rQshiftStr(4);
988 const cap_signature
= this._sock
.rQshiftStr(8);
989 serverSupportedTunnelTypes
[cap_code
] = { vendor
: cap_vendor
, signature
: cap_signature
};
992 Log
.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes
);
994 // Siemens touch panels have a VNC server that supports NOTUNNEL,
995 // but forgets to advertise it. Try to detect such servers by
996 // looking for their custom tunnel type.
997 if (serverSupportedTunnelTypes
[1] &&
998 (serverSupportedTunnelTypes
[1].vendor
=== "SICR") &&
999 (serverSupportedTunnelTypes
[1].signature
=== "SCHANNEL")) {
1000 Log
.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
1001 serverSupportedTunnelTypes
[0] = { vendor
: 'TGHT', signature
: 'NOTUNNEL' };
1004 // choose the notunnel type
1005 if (serverSupportedTunnelTypes
[0]) {
1006 if (serverSupportedTunnelTypes
[0].vendor
!= clientSupportedTunnelTypes
[0].vendor
||
1007 serverSupportedTunnelTypes
[0].signature
!= clientSupportedTunnelTypes
[0].signature
) {
1008 return this._fail("Client's tunnel type had the incorrect " +
1009 "vendor or signature");
1011 Log
.Debug("Selected tunnel type: " + clientSupportedTunnelTypes
[0]);
1012 this._sock
.send([0, 0, 0, 0]); // use NOTUNNEL
1013 return false; // wait until we receive the sub auth count to continue
1015 return this._fail("Server wanted tunnels, but doesn't support " +
1016 "the notunnel type");
1020 _negotiate_tight_auth() {
1021 if (!this._rfb_tightvnc
) { // first pass, do the tunnel negotiation
1022 if (this._sock
.rQwait("num tunnels", 4)) { return false; }
1023 const numTunnels
= this._sock
.rQshift32();
1024 if (numTunnels
> 0 && this._sock
.rQwait("tunnel capabilities", 16 * numTunnels
, 4)) { return false; }
1026 this._rfb_tightvnc
= true;
1028 if (numTunnels
> 0) {
1029 this._negotiate_tight_tunnels(numTunnels
);
1030 return false; // wait until we receive the sub auth to continue
1034 // second pass, do the sub-auth negotiation
1035 if (this._sock
.rQwait("sub auth count", 4)) { return false; }
1036 const subAuthCount
= this._sock
.rQshift32();
1037 if (subAuthCount
=== 0) { // empty sub-auth list received means 'no auth' subtype selected
1038 this._rfb_init_state
= 'SecurityResult';
1042 if (this._sock
.rQwait("sub auth capabilities", 16 * subAuthCount
, 4)) { return false; }
1044 const clientSupportedTypes
= {
1049 const serverSupportedTypes
= [];
1051 for (let i
= 0; i
< subAuthCount
; i
++) {
1052 this._sock
.rQshift32(); // capNum
1053 const capabilities
= this._sock
.rQshiftStr(12);
1054 serverSupportedTypes
.push(capabilities
);
1057 Log
.Debug("Server Tight authentication types: " + serverSupportedTypes
);
1059 for (let authType
in clientSupportedTypes
) {
1060 if (serverSupportedTypes
.indexOf(authType
) != -1) {
1061 this._sock
.send([0, 0, 0, clientSupportedTypes
[authType
]]);
1062 Log
.Debug("Selected authentication type: " + authType
);
1065 case 'STDVNOAUTH__': // no auth
1066 this._rfb_init_state
= 'SecurityResult';
1068 case 'STDVVNCAUTH_': // VNC auth
1069 this._rfb_auth_scheme
= 2;
1070 return this._init_msg();
1072 return this._fail("Unsupported tiny auth scheme " +
1073 "(scheme: " + authType
+ ")");
1078 return this._fail("No supported sub-auth types!");
1081 _negotiate_authentication() {
1082 switch (this._rfb_auth_scheme
) {
1084 if (this._rfb_version
>= 3.8) {
1085 this._rfb_init_state
= 'SecurityResult';
1088 this._rfb_init_state
= 'ClientInitialisation';
1089 return this._init_msg();
1091 case 22: // XVP auth
1092 return this._negotiate_xvp_auth();
1094 case 2: // VNC authentication
1095 return this._negotiate_std_vnc_auth();
1097 case 16: // TightVNC Security Type
1098 return this._negotiate_tight_auth();
1101 return this._fail("Unsupported auth scheme (scheme: " +
1102 this._rfb_auth_scheme
+ ")");
1106 _handle_security_result() {
1107 if (this._sock
.rQwait('VNC auth response ', 4)) { return false; }
1109 const status
= this._sock
.rQshift32();
1111 if (status
=== 0) { // OK
1112 this._rfb_init_state
= 'ClientInitialisation';
1113 Log
.Debug('Authentication OK');
1114 return this._init_msg();
1116 if (this._rfb_version
>= 3.8) {
1117 this._rfb_init_state
= "SecurityReason";
1118 this._security_context
= "security result";
1119 this._security_status
= status
;
1120 return this._init_msg();
1122 this.dispatchEvent(new CustomEvent(
1124 { detail
: { status
: status
} }));
1126 return this._fail("Security handshake failed");
1131 _negotiate_server_init() {
1132 if (this._sock
.rQwait("server initialization", 24)) { return false; }
1135 const width
= this._sock
.rQshift16();
1136 const height
= this._sock
.rQshift16();
1139 const bpp
= this._sock
.rQshift8();
1140 const depth
= this._sock
.rQshift8();
1141 const big_endian
= this._sock
.rQshift8();
1142 const true_color
= this._sock
.rQshift8();
1144 const red_max
= this._sock
.rQshift16();
1145 const green_max
= this._sock
.rQshift16();
1146 const blue_max
= this._sock
.rQshift16();
1147 const red_shift
= this._sock
.rQshift8();
1148 const green_shift
= this._sock
.rQshift8();
1149 const blue_shift
= this._sock
.rQshift8();
1150 this._sock
.rQskipBytes(3); // padding
1152 // NB(directxman12): we don't want to call any callbacks or print messages until
1153 // *after* we're past the point where we could backtrack
1155 /* Connection name/title */
1156 const name_length = this._sock.rQshift32();
1157 if (this._sock.rQwait('server init name', name_length, 24)) { return false; }
1158 let name = decodeUTF8(this._sock.rQshiftStr(name_length));
1160 if (this._rfb_tightvnc) {
1161 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; }
1162 // In TightVNC mode, ServerInit message is extended
1163 const numServerMessages = this._sock.rQshift16();
1164 const numClientMessages = this._sock.rQshift16();
1165 const numEncodings = this._sock.rQshift16();
1166 this._sock.rQskipBytes(2); // padding
1168 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
1169 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; }
1171 // we don't actually do anything with the capability information that TIGHT sends,
1172 // so we just skip the all of this.
1174 // TIGHT server message capabilities
1175 this._sock.rQskipBytes(16 * numServerMessages);
1177 // TIGHT client message capabilities
1178 this._sock.rQskipBytes(16 * numClientMessages);
1180 // TIGHT encoding capabilities
1181 this._sock.rQskipBytes(16 * numEncodings);
1184 // NB(directxman12): these are down here so that we don't run them multiple times
1186 Log.Info("Screen: " + width + "x" + height +
1187 ", bpp: " + bpp + ", depth: " + depth +
1188 ", big_endian: " + big_endian +
1189 ", true_color: " + true_color +
1190 ", red_max: " + red_max +
1191 ", green_max: " + green_max +
1192 ", blue_max: " + blue_max +
1193 ", red_shift: " + red_shift +
1194 ", green_shift: " + green_shift +
1195 ", blue_shift: " + blue_shift);
1197 // we're past the point where we could backtrack, so it's safe to call this
1198 this._setDesktopName(name);
1199 this._resize(width, height);
1201 if (!this._viewOnly) { this._keyboard.grab(); }
1202 if (!this._viewOnly) { this._mouse.grab(); }
1204 this._fb_depth = 24;
1206 if (this._fb_name === "Intel(r) AMT KVM") {
1207 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
1211 RFB.messages.pixelFormat(this._sock, this._fb_depth, true);
1212 this._sendEncodings();
1213 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height);
1215 this._updateConnectionState('connected');
1222 // In preference order
1223 encs.push(encodings.encodingCopyRect);
1224 // Only supported with full depth support
1225 if (this._fb_depth == 24) {
1226 encs.push(encodings.encodingTight);
1227 encs.push(encodings.encodingTightPNG);
1228 encs.push(encodings.encodingHextile);
1229 encs.push(encodings.encodingRRE);
1231 encs.push(encodings.encodingRaw);
1233 // Psuedo-encoding settings
1234 encs.push(encodings.pseudoEncodingQualityLevel0 + 6);
1235 encs.push(encodings.pseudoEncodingCompressLevel0 + 2);
1237 encs.push(encodings.pseudoEncodingDesktopSize);
1238 encs.push(encodings.pseudoEncodingLastRect);
1239 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
1240 encs.push(encodings.pseudoEncodingExtendedDesktopSize);
1241 encs.push(encodings.pseudoEncodingXvp);
1242 encs.push(encodings.pseudoEncodingFence);
1243 encs.push(encodings.pseudoEncodingContinuousUpdates);
1244 encs.push(encodings.pseudoEncodingDesktopName);
1246 if (this._fb_depth == 24) {
1247 encs.push(encodings.pseudoEncodingVMwareCursor);
1248 encs.push(encodings.pseudoEncodingCursor);
1251 RFB.messages.clientEncodings(this._sock, encs);
1254 /* RFB protocol initialization states:
1259 * ClientInitialization - not triggered by server message
1260 * ServerInitialization
1263 switch (this._rfb_init_state
) {
1264 case 'ProtocolVersion':
1265 return this._negotiate_protocol_version();
1268 return this._negotiate_security();
1270 case 'Authentication':
1271 return this._negotiate_authentication();
1273 case 'SecurityResult':
1274 return this._handle_security_result();
1276 case 'SecurityReason':
1277 return this._handle_security_reason();
1279 case 'ClientInitialisation':
1280 this._sock
.send([this._shared
? 1 : 0]); // ClientInitialisation
1281 this._rfb_init_state
= 'ServerInitialisation';
1284 case 'ServerInitialisation':
1285 return this._negotiate_server_init();
1288 return this._fail("Unknown init state (state: " +
1289 this._rfb_init_state
+ ")");
1293 _handle_set_colour_map_msg() {
1294 Log
.Debug("SetColorMapEntries");
1296 return this._fail("Unexpected SetColorMapEntries message");
1299 _handle_server_cut_text() {
1300 Log
.Debug("ServerCutText");
1302 if (this._sock
.rQwait("ServerCutText header", 7, 1)) { return false; }
1303 this._sock
.rQskipBytes(3); // Padding
1304 const length
= this._sock
.rQshift32();
1305 if (this._sock
.rQwait("ServerCutText", length
, 8)) { return false; }
1307 const text
= this._sock
.rQshiftStr(length
);
1309 if (this._viewOnly
) { return true; }
1311 this.dispatchEvent(new CustomEvent(
1313 { detail
: { text
: text
} }));
1318 _handle_server_fence_msg() {
1319 if (this._sock
.rQwait("ServerFence header", 8, 1)) { return false; }
1320 this._sock
.rQskipBytes(3); // Padding
1321 let flags
= this._sock
.rQshift32();
1322 let length
= this._sock
.rQshift8();
1324 if (this._sock
.rQwait("ServerFence payload", length
, 9)) { return false; }
1327 Log
.Warn("Bad payload length (" + length
+ ") in fence response");
1331 const payload
= this._sock
.rQshiftStr(length
);
1333 this._supportsFence
= true;
1338 * (1<<0) - BlockBefore
1339 * (1<<1) - BlockAfter
1344 if (!(flags
& (1<<31))) {
1345 return this._fail("Unexpected fence response");
1348 // Filter out unsupported flags
1349 // FIXME: support syncNext
1350 flags
&= (1<<0) | (1<<1);
1352 // BlockBefore and BlockAfter are automatically handled by
1353 // the fact that we process each incoming message
1355 RFB
.messages
.clientFence(this._sock
, flags
, payload
);
1361 if (this._sock
.rQwait("XVP version and message", 3, 1)) { return false; }
1362 this._sock
.rQskipBytes(1); // Padding
1363 const xvp_ver
= this._sock
.rQshift8();
1364 const xvp_msg
= this._sock
.rQshift8();
1368 Log
.Error("XVP Operation Failed");
1371 this._rfb_xvp_ver
= xvp_ver
;
1372 Log
.Info("XVP extensions enabled (version " + this._rfb_xvp_ver
+ ")");
1373 this._setCapability("power", true);
1376 this._fail("Illegal server XVP message (msg: " + xvp_msg
+ ")");
1385 if (this._FBU
.rects
> 0) {
1388 msg_type
= this._sock
.rQshift8();
1393 case 0: // FramebufferUpdate
1394 ret
= this._framebufferUpdate();
1395 if (ret
&& !this._enabledContinuousUpdates
) {
1396 RFB
.messages
.fbUpdateRequest(this._sock
, true, 0, 0,
1397 this._fb_width
, this._fb_height
);
1401 case 1: // SetColorMapEntries
1402 return this._handle_set_colour_map_msg();
1406 this.dispatchEvent(new CustomEvent(
1411 case 3: // ServerCutText
1412 return this._handle_server_cut_text();
1414 case 150: // EndOfContinuousUpdates
1415 first
= !this._supportsContinuousUpdates
;
1416 this._supportsContinuousUpdates
= true;
1417 this._enabledContinuousUpdates
= false;
1419 this._enabledContinuousUpdates
= true;
1420 this._updateContinuousUpdates();
1421 Log
.Info("Enabling continuous updates.");
1423 // FIXME: We need to send a framebufferupdaterequest here
1424 // if we add support for turning off continuous updates
1428 case 248: // ServerFence
1429 return this._handle_server_fence_msg();
1432 return this._handle_xvp_msg();
1435 this._fail("Unexpected server message (type " + msg_type
+ ")");
1436 Log
.Debug("sock.rQslice(0, 30): " + this._sock
.rQslice(0, 30));
1442 this._flushing
= false;
1443 // Resume processing
1444 if (this._sock
.rQlen
> 0) {
1445 this._handle_message();
1449 _framebufferUpdate() {
1450 if (this._FBU
.rects
=== 0) {
1451 if (this._sock
.rQwait("FBU header", 3, 1)) { return false; }
1452 this._sock
.rQskipBytes(1); // Padding
1453 this._FBU
.rects
= this._sock
.rQshift16();
1455 // Make sure the previous frame is fully rendered first
1456 // to avoid building up an excessive queue
1457 if (this._display
.pending()) {
1458 this._flushing
= true;
1459 this._display
.flush();
1464 while (this._FBU
.rects
> 0) {
1465 if (this._FBU
.encoding
=== null) {
1466 if (this._sock
.rQwait("rect header", 12)) { return false; }
1467 /* New FramebufferUpdate */
1469 const hdr
= this._sock
.rQshiftBytes(12);
1470 this._FBU
.x
= (hdr
[0] << 8) + hdr
[1];
1471 this._FBU
.y
= (hdr
[2] << 8) + hdr
[3];
1472 this._FBU
.width
= (hdr
[4] << 8) + hdr
[5];
1473 this._FBU
.height
= (hdr
[6] << 8) + hdr
[7];
1474 this._FBU
.encoding
= parseInt((hdr
[8] << 24) + (hdr
[9] << 16) +
1475 (hdr
[10] << 8) + hdr
[11], 10);
1478 if (!this._handleRect()) {
1483 this._FBU
.encoding
= null;
1486 this._display
.flip();
1488 return true; // We finished this FBU
1492 switch (this._FBU
.encoding
) {
1493 case encodings
.pseudoEncodingLastRect
:
1494 this._FBU
.rects
= 1; // Will be decreased when we return
1497 case encodings
.pseudoEncodingVMwareCursor
:
1498 return this._handleVMwareCursor();
1500 case encodings
.pseudoEncodingCursor
:
1501 return this._handleCursor();
1503 case encodings
.pseudoEncodingQEMUExtendedKeyEvent
:
1504 // Old Safari doesn't support creating keyboard events
1506 const keyboardEvent
= document
.createEvent("keyboardEvent");
1507 if (keyboardEvent
.code
!== undefined) {
1508 this._qemuExtKeyEventSupported
= true;
1515 case encodings
.pseudoEncodingDesktopName
:
1516 return this._handleDesktopName();
1518 case encodings
.pseudoEncodingDesktopSize
:
1519 this._resize(this._FBU
.width
, this._FBU
.height
);
1522 case encodings
.pseudoEncodingExtendedDesktopSize
:
1523 return this._handleExtendedDesktopSize();
1526 return this._handleDataRect();
1530 _handleVMwareCursor() {
1531 const hotx
= this._FBU
.x
; // hotspot-x
1532 const hoty
= this._FBU
.y
; // hotspot-y
1533 const w
= this._FBU
.width
;
1534 const h
= this._FBU
.height
;
1535 if (this._sock
.rQwait("VMware cursor encoding", 1)) {
1539 const cursor_type
= this._sock
.rQshift8();
1541 this._sock
.rQshift8(); //Padding
1544 const bytesPerPixel
= 4;
1547 if (cursor_type
== 0) {
1548 //Used to filter away unimportant bits.
1549 //OR is used for correct conversion in js.
1550 const PIXEL_MASK
= 0xffffff00 | 0;
1551 rgba
= new Array(w
* h
* bytesPerPixel
);
1553 if (this._sock
.rQwait("VMware cursor classic encoding",
1554 (w
* h
* bytesPerPixel
) * 2, 2)) {
1558 let and_mask
= new Array(w
* h
);
1559 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
1560 and_mask
[pixel
] = this._sock
.rQshift32();
1563 let xor_mask
= new Array(w
* h
);
1564 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
1565 xor_mask
[pixel
] = this._sock
.rQshift32();
1568 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
1569 if (and_mask
[pixel
] == 0) {
1570 //Fully opaque pixel
1571 let bgr
= xor_mask
[pixel
];
1572 let r
= bgr
>> 8 & 0xff;
1573 let g
= bgr
>> 16 & 0xff;
1574 let b
= bgr
>> 24 & 0xff;
1576 rgba
[(pixel
* bytesPerPixel
) ] = r
; //r
1577 rgba
[(pixel
* bytesPerPixel
) + 1 ] = g
; //g
1578 rgba
[(pixel
* bytesPerPixel
) + 2 ] = b
; //b
1579 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff; //a
1581 } else if ((and_mask
[pixel
] & PIXEL_MASK
) ==
1583 //Only screen value matters, no mouse colouring
1584 if (xor_mask
[pixel
] == 0) {
1586 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
1587 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
1588 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
1589 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0x00;
1591 } else if ((xor_mask
[pixel
] & PIXEL_MASK
) ==
1593 //Inverted pixel, not supported in browsers.
1594 //Fully opaque instead.
1595 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
1596 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
1597 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
1598 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
1601 //Unhandled xor_mask
1602 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
1603 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
1604 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
1605 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
1609 //Unhandled and_mask
1610 rgba
[(pixel
* bytesPerPixel
) ] = 0x00;
1611 rgba
[(pixel
* bytesPerPixel
) + 1 ] = 0x00;
1612 rgba
[(pixel
* bytesPerPixel
) + 2 ] = 0x00;
1613 rgba
[(pixel
* bytesPerPixel
) + 3 ] = 0xff;
1618 } else if (cursor_type
== 1) {
1619 if (this._sock
.rQwait("VMware cursor alpha encoding",
1624 rgba
= new Array(w
* h
* bytesPerPixel
);
1626 for (let pixel
= 0; pixel
< (w
* h
); pixel
++) {
1627 let data
= this._sock
.rQshift32();
1629 rgba
[(pixel
* 4) ] = data
>> 8 & 0xff; //r
1630 rgba
[(pixel
* 4) + 1 ] = data
>> 16 & 0xff; //g
1631 rgba
[(pixel
* 4) + 2 ] = data
>> 24 & 0xff; //b
1632 rgba
[(pixel
* 4) + 3 ] = data
& 0xff; //a
1636 Log
.Warn("The given cursor type is not supported: "
1637 + cursor_type
+ " given.");
1641 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
1647 const hotx
= this._FBU
.x
; // hotspot-x
1648 const hoty
= this._FBU
.y
; // hotspot-y
1649 const w
= this._FBU
.width
;
1650 const h
= this._FBU
.height
;
1652 const pixelslength
= w
* h
* 4;
1653 const masklength
= Math
.ceil(w
/ 8) * h
;
1655 let bytes
= pixelslength
+ masklength
;
1656 if (this._sock
.rQwait("cursor encoding", bytes
)) {
1660 // Decode from BGRX pixels + bit mask to RGBA
1661 const pixels
= this._sock
.rQshiftBytes(pixelslength
);
1662 const mask
= this._sock
.rQshiftBytes(masklength
);
1663 let rgba
= new Uint8Array(w
* h
* 4);
1666 for (let y
= 0; y
< h
; y
++) {
1667 for (let x
= 0; x
< w
; x
++) {
1668 let mask_idx
= y
* Math
.ceil(w
/ 8) + Math
.floor(x
/ 8);
1669 let alpha
= (mask
[mask_idx
] << (x
% 8)) & 0x80 ? 255 : 0;
1670 rgba
[pix_idx
] = pixels
[pix_idx
+ 2];
1671 rgba
[pix_idx
+ 1] = pixels
[pix_idx
+ 1];
1672 rgba
[pix_idx
+ 2] = pixels
[pix_idx
];
1673 rgba
[pix_idx
+ 3] = alpha
;
1678 this._updateCursor(rgba
, hotx
, hoty
, w
, h
);
1683 _handleDesktopName() {
1684 if (this._sock
.rQwait("DesktopName", 4)) {
1688 let length
= this._sock
.rQshift32();
1690 if (this._sock
.rQwait("DesktopName", length
, 4)) {
1694 let name
= this._sock
.rQshiftStr(length
);
1695 name
= decodeUTF8(name
);
1697 this._setDesktopName(name
);
1702 _handleExtendedDesktopSize() {
1703 if (this._sock
.rQwait("ExtendedDesktopSize", 4)) {
1707 const number_of_screens
= this._sock
.rQpeek8();
1709 let bytes
= 4 + (number_of_screens
* 16);
1710 if (this._sock
.rQwait("ExtendedDesktopSize", bytes
)) {
1714 const firstUpdate
= !this._supportsSetDesktopSize
;
1715 this._supportsSetDesktopSize
= true;
1717 // Normally we only apply the current resize mode after a
1718 // window resize event. However there is no such trigger on the
1719 // initial connect. And we don't know if the server supports
1720 // resizing until we've gotten here.
1722 this._requestRemoteResize();
1725 this._sock
.rQskipBytes(1); // number-of-screens
1726 this._sock
.rQskipBytes(3); // padding
1728 for (let i
= 0; i
< number_of_screens
; i
+= 1) {
1729 // Save the id and flags of the first screen
1731 this._screen_id
= this._sock
.rQshiftBytes(4); // id
1732 this._sock
.rQskipBytes(2); // x-position
1733 this._sock
.rQskipBytes(2); // y-position
1734 this._sock
.rQskipBytes(2); // width
1735 this._sock
.rQskipBytes(2); // height
1736 this._screen_flags
= this._sock
.rQshiftBytes(4); // flags
1738 this._sock
.rQskipBytes(16);
1743 * The x-position indicates the reason for the change:
1745 * 0 - server resized on its own
1746 * 1 - this client requested the resize
1747 * 2 - another client requested the resize
1750 // We need to handle errors when we requested the resize.
1751 if (this._FBU
.x
=== 1 && this._FBU
.y
!== 0) {
1753 // The y-position indicates the status code from the server
1754 switch (this._FBU
.y
) {
1756 msg
= "Resize is administratively prohibited";
1759 msg
= "Out of resources";
1762 msg
= "Invalid screen layout";
1765 msg
= "Unknown reason";
1768 Log
.Warn("Server did not accept the resize request: "
1771 this._resize(this._FBU
.width
, this._FBU
.height
);
1778 let decoder
= this._decoders
[this._FBU
.encoding
];
1780 this._fail("Unsupported encoding (encoding: " +
1781 this._FBU
.encoding
+ ")");
1786 return decoder
.decodeRect(this._FBU
.x
, this._FBU
.y
,
1787 this._FBU
.width
, this._FBU
.height
,
1788 this._sock
, this._display
,
1791 this._fail("Error decoding rect: " + err
);
1796 _updateContinuousUpdates() {
1797 if (!this._enabledContinuousUpdates
) { return; }
1799 RFB
.messages
.enableContinuousUpdates(this._sock
, true, 0, 0,
1800 this._fb_width
, this._fb_height
);
1803 _resize(width
, height
) {
1804 this._fb_width
= width
;
1805 this._fb_height
= height
;
1807 this._display
.resize(this._fb_width
, this._fb_height
);
1809 // Adjust the visible viewport based on the new dimensions
1811 this._updateScale();
1813 this._updateContinuousUpdates();
1817 if (this._rfb_xvp_ver
< ver
) { return; }
1818 Log
.Info("Sending XVP operation " + op
+ " (version " + ver
+ ")");
1819 RFB
.messages
.xvpOp(this._sock
, ver
, op
);
1822 _updateCursor(rgba
, hotx
, hoty
, w
, h
) {
1823 this._cursorImage
= {
1825 hotx
: hotx
, hoty
: hoty
, w
: w
, h
: h
,
1827 this._refreshCursor();
1830 _shouldShowDotCursor() {
1831 // Called when this._cursorImage is updated
1832 if (!this._showDotCursor
) {
1833 // User does not want to see the dot, so...
1837 // The dot should not be shown if the cursor is already visible,
1838 // i.e. contains at least one not-fully-transparent pixel.
1839 // So iterate through all alpha bytes in rgba and stop at the
1841 for (let i
= 3; i
< this._cursorImage
.rgbaPixels
.length
; i
+= 4) {
1842 if (this._cursorImage
.rgbaPixels
[i
]) {
1847 // At this point, we know that the cursor is fully transparent, and
1848 // the user wants to see the dot instead of this.
1853 if (this._rfb_connection_state
!== 'connected') { return; }
1854 const image
= this._shouldShowDotCursor() ? RFB
.cursors
.dot
: this._cursorImage
;
1855 this._cursor
.change(image
.rgbaPixels
,
1856 image
.hotx
, image
.hoty
,
1861 static genDES(password
, challenge
) {
1862 const passwordChars
= password
.split('').map(c
=> c
.charCodeAt(0));
1863 return (new DES(passwordChars
)).encrypt(challenge
);
1869 keyEvent(sock
, keysym
, down
) {
1870 const buff
= sock
._sQ
;
1871 const offset
= sock
._sQlen
;
1873 buff
[offset
] = 4; // msg-type
1874 buff
[offset
+ 1] = down
;
1876 buff
[offset
+ 2] = 0;
1877 buff
[offset
+ 3] = 0;
1879 buff
[offset
+ 4] = (keysym
>> 24);
1880 buff
[offset
+ 5] = (keysym
>> 16);
1881 buff
[offset
+ 6] = (keysym
>> 8);
1882 buff
[offset
+ 7] = keysym
;
1888 QEMUExtendedKeyEvent(sock
, keysym
, down
, keycode
) {
1889 function getRFBkeycode(xt_scancode
) {
1890 const upperByte
= (keycode
>> 8);
1891 const lowerByte
= (keycode
& 0x00ff);
1892 if (upperByte
=== 0xe0 && lowerByte
< 0x7f) {
1893 return lowerByte
| 0x80;
1898 const buff
= sock
._sQ
;
1899 const offset
= sock
._sQlen
;
1901 buff
[offset
] = 255; // msg-type
1902 buff
[offset
+ 1] = 0; // sub msg-type
1904 buff
[offset
+ 2] = (down
>> 8);
1905 buff
[offset
+ 3] = down
;
1907 buff
[offset
+ 4] = (keysym
>> 24);
1908 buff
[offset
+ 5] = (keysym
>> 16);
1909 buff
[offset
+ 6] = (keysym
>> 8);
1910 buff
[offset
+ 7] = keysym
;
1912 const RFBkeycode
= getRFBkeycode(keycode
);
1914 buff
[offset
+ 8] = (RFBkeycode
>> 24);
1915 buff
[offset
+ 9] = (RFBkeycode
>> 16);
1916 buff
[offset
+ 10] = (RFBkeycode
>> 8);
1917 buff
[offset
+ 11] = RFBkeycode
;
1923 pointerEvent(sock
, x
, y
, mask
) {
1924 const buff
= sock
._sQ
;
1925 const offset
= sock
._sQlen
;
1927 buff
[offset
] = 5; // msg-type
1929 buff
[offset
+ 1] = mask
;
1931 buff
[offset
+ 2] = x
>> 8;
1932 buff
[offset
+ 3] = x
;
1934 buff
[offset
+ 4] = y
>> 8;
1935 buff
[offset
+ 5] = y
;
1941 // TODO(directxman12): make this unicode compatible?
1942 clientCutText(sock
, text
) {
1943 const buff
= sock
._sQ
;
1944 const offset
= sock
._sQlen
;
1946 buff
[offset
] = 6; // msg-type
1948 buff
[offset
+ 1] = 0; // padding
1949 buff
[offset
+ 2] = 0; // padding
1950 buff
[offset
+ 3] = 0; // padding
1952 let length
= text
.length
;
1954 buff
[offset
+ 4] = length
>> 24;
1955 buff
[offset
+ 5] = length
>> 16;
1956 buff
[offset
+ 6] = length
>> 8;
1957 buff
[offset
+ 7] = length
;
1961 // We have to keep track of from where in the text we begin creating the
1962 // buffer for the flush in the next iteration.
1965 let remaining
= length
;
1966 while (remaining
> 0) {
1968 let flushSize
= Math
.min(remaining
, (sock
._sQbufferSize
- sock
._sQlen
));
1969 for (let i
= 0; i
< flushSize
; i
++) {
1970 buff
[sock
._sQlen
+ i
] = text
.charCodeAt(textOffset
+ i
);
1973 sock
._sQlen
+= flushSize
;
1976 remaining
-= flushSize
;
1977 textOffset
+= flushSize
;
1981 setDesktopSize(sock
, width
, height
, id
, flags
) {
1982 const buff
= sock
._sQ
;
1983 const offset
= sock
._sQlen
;
1985 buff
[offset
] = 251; // msg-type
1986 buff
[offset
+ 1] = 0; // padding
1987 buff
[offset
+ 2] = width
>> 8; // width
1988 buff
[offset
+ 3] = width
;
1989 buff
[offset
+ 4] = height
>> 8; // height
1990 buff
[offset
+ 5] = height
;
1992 buff
[offset
+ 6] = 1; // number-of-screens
1993 buff
[offset
+ 7] = 0; // padding
1996 buff
[offset
+ 8] = id
>> 24; // id
1997 buff
[offset
+ 9] = id
>> 16;
1998 buff
[offset
+ 10] = id
>> 8;
1999 buff
[offset
+ 11] = id
;
2000 buff
[offset
+ 12] = 0; // x-position
2001 buff
[offset
+ 13] = 0;
2002 buff
[offset
+ 14] = 0; // y-position
2003 buff
[offset
+ 15] = 0;
2004 buff
[offset
+ 16] = width
>> 8; // width
2005 buff
[offset
+ 17] = width
;
2006 buff
[offset
+ 18] = height
>> 8; // height
2007 buff
[offset
+ 19] = height
;
2008 buff
[offset
+ 20] = flags
>> 24; // flags
2009 buff
[offset
+ 21] = flags
>> 16;
2010 buff
[offset
+ 22] = flags
>> 8;
2011 buff
[offset
+ 23] = flags
;
2017 clientFence(sock
, flags
, payload
) {
2018 const buff
= sock
._sQ
;
2019 const offset
= sock
._sQlen
;
2021 buff
[offset
] = 248; // msg-type
2023 buff
[offset
+ 1] = 0; // padding
2024 buff
[offset
+ 2] = 0; // padding
2025 buff
[offset
+ 3] = 0; // padding
2027 buff
[offset
+ 4] = flags
>> 24; // flags
2028 buff
[offset
+ 5] = flags
>> 16;
2029 buff
[offset
+ 6] = flags
>> 8;
2030 buff
[offset
+ 7] = flags
;
2032 const n
= payload
.length
;
2034 buff
[offset
+ 8] = n
; // length
2036 for (let i
= 0; i
< n
; i
++) {
2037 buff
[offset
+ 9 + i
] = payload
.charCodeAt(i
);
2040 sock
._sQlen
+= 9 + n
;
2044 enableContinuousUpdates(sock
, enable
, x
, y
, width
, height
) {
2045 const buff
= sock
._sQ
;
2046 const offset
= sock
._sQlen
;
2048 buff
[offset
] = 150; // msg-type
2049 buff
[offset
+ 1] = enable
; // enable-flag
2051 buff
[offset
+ 2] = x
>> 8; // x
2052 buff
[offset
+ 3] = x
;
2053 buff
[offset
+ 4] = y
>> 8; // y
2054 buff
[offset
+ 5] = y
;
2055 buff
[offset
+ 6] = width
>> 8; // width
2056 buff
[offset
+ 7] = width
;
2057 buff
[offset
+ 8] = height
>> 8; // height
2058 buff
[offset
+ 9] = height
;
2064 pixelFormat(sock
, depth
, true_color
) {
2065 const buff
= sock
._sQ
;
2066 const offset
= sock
._sQlen
;
2072 } else if (depth
> 8) {
2078 const bits
= Math
.floor(depth
/3);
2080 buff
[offset
] = 0; // msg-type
2082 buff
[offset
+ 1] = 0; // padding
2083 buff
[offset
+ 2] = 0; // padding
2084 buff
[offset
+ 3] = 0; // padding
2086 buff
[offset
+ 4] = bpp
; // bits-per-pixel
2087 buff
[offset
+ 5] = depth
; // depth
2088 buff
[offset
+ 6] = 0; // little-endian
2089 buff
[offset
+ 7] = true_color
? 1 : 0; // true-color
2091 buff
[offset
+ 8] = 0; // red-max
2092 buff
[offset
+ 9] = (1 << bits
) - 1; // red-max
2094 buff
[offset
+ 10] = 0; // green-max
2095 buff
[offset
+ 11] = (1 << bits
) - 1; // green-max
2097 buff
[offset
+ 12] = 0; // blue-max
2098 buff
[offset
+ 13] = (1 << bits
) - 1; // blue-max
2100 buff
[offset
+ 14] = bits
* 2; // red-shift
2101 buff
[offset
+ 15] = bits
* 1; // green-shift
2102 buff
[offset
+ 16] = bits
* 0; // blue-shift
2104 buff
[offset
+ 17] = 0; // padding
2105 buff
[offset
+ 18] = 0; // padding
2106 buff
[offset
+ 19] = 0; // padding
2112 clientEncodings(sock
, encodings
) {
2113 const buff
= sock
._sQ
;
2114 const offset
= sock
._sQlen
;
2116 buff
[offset
] = 2; // msg-type
2117 buff
[offset
+ 1] = 0; // padding
2119 buff
[offset
+ 2] = encodings
.length
>> 8;
2120 buff
[offset
+ 3] = encodings
.length
;
2123 for (let i
= 0; i
< encodings
.length
; i
++) {
2124 const enc
= encodings
[i
];
2125 buff
[j
] = enc
>> 24;
2126 buff
[j
+ 1] = enc
>> 16;
2127 buff
[j
+ 2] = enc
>> 8;
2133 sock
._sQlen
+= j
- offset
;
2137 fbUpdateRequest(sock
, incremental
, x
, y
, w
, h
) {
2138 const buff
= sock
._sQ
;
2139 const offset
= sock
._sQlen
;
2141 if (typeof(x
) === "undefined") { x
= 0; }
2142 if (typeof(y
) === "undefined") { y
= 0; }
2144 buff
[offset
] = 3; // msg-type
2145 buff
[offset
+ 1] = incremental
? 1 : 0;
2147 buff
[offset
+ 2] = (x
>> 8) & 0xFF;
2148 buff
[offset
+ 3] = x
& 0xFF;
2150 buff
[offset
+ 4] = (y
>> 8) & 0xFF;
2151 buff
[offset
+ 5] = y
& 0xFF;
2153 buff
[offset
+ 6] = (w
>> 8) & 0xFF;
2154 buff
[offset
+ 7] = w
& 0xFF;
2156 buff
[offset
+ 8] = (h
>> 8) & 0xFF;
2157 buff
[offset
+ 9] = h
& 0xFF;
2163 xvpOp(sock
, ver
, op
) {
2164 const buff
= sock
._sQ
;
2165 const offset
= sock
._sQlen
;
2167 buff
[offset
] = 250; // msg-type
2168 buff
[offset
+ 1] = 0; // padding
2170 buff
[offset
+ 2] = ver
;
2171 buff
[offset
+ 3] = op
;
2180 rgbaPixels
: new Uint8Array(),
2186 /* eslint-disable indent */
2187 rgbaPixels
: new Uint8Array([
2188 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2189 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
2190 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
2192 /* eslint-enable indent */