Nicolas Vazquez eac357cb77
kvm: Secure KVM VNC Console Access Using the CA Framework (#7015)
This PR allows securing the console access through CloudStack to the virtual machines running on KVM. The secure access is achieved through the generated certificates for the CA Framework in CloudStack, that provides mutual TLS connections between agents. These certificates are used to also secure the connection between the console proxies and the VNC ports for VM console access.

This feature is only supported on the KVM hypervisor

Design Document: https://cwiki.apache.org/confluence/display/CLOUDSTACK/Secure+KVM+VNC+connection+using+the+CA+framework
2023-01-27 17:22:06 +05:30

3007 lines
104 KiB
JavaScript

/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
*/
import { toUnsigned32bit, toSigned32bit } from './util/int.js';
import * as Log from './util/logging.js';
import { encodeUTF8, decodeUTF8 } from './util/strings.js';
import { dragThreshold } from './util/browser.js';
import { clientToElement } from './util/element.js';
import { setCapture } from './util/events.js';
import EventTargetMixin from './util/eventtarget.js';
import Display from "./display.js";
import Inflator from "./inflator.js";
import Deflator from "./deflator.js";
import Keyboard from "./input/keyboard.js";
import GestureHandler from "./input/gesturehandler.js";
import Cursor from "./util/cursor.js";
import Websock from "./websock.js";
import DES from "./des.js";
import KeyTable from "./input/keysym.js";
import USKeyTable from "./input/uskeysym.js";
import XtScancode from "./input/xtscancodes.js";
import { encodings } from "./encodings.js";
import "./util/polyfill.js";
import RawDecoder from "./decoders/raw.js";
import CopyRectDecoder from "./decoders/copyrect.js";
import RREDecoder from "./decoders/rre.js";
import HextileDecoder from "./decoders/hextile.js";
import TightDecoder from "./decoders/tight.js";
import TightPNGDecoder from "./decoders/tightpng.js";
// How many seconds to wait for a disconnect to finish
const DISCONNECT_TIMEOUT = 3;
const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
// Minimum wait (ms) between two mouse moves
const MOUSE_MOVE_DELAY = 17;
// Wheel thresholds
const WHEEL_STEP = 50; // Pixels needed for one step
const WHEEL_LINE_HEIGHT = 19; // Assumed pixels for one line step
// Gesture thresholds
const GESTURE_ZOOMSENS = 75;
const GESTURE_SCRLSENS = 50;
const DOUBLE_TAP_TIMEOUT = 1000;
const DOUBLE_TAP_THRESHOLD = 50;
// Extended clipboard pseudo-encoding formats
const extendedClipboardFormatText = 1;
/*eslint-disable no-unused-vars */
const extendedClipboardFormatRtf = 1 << 1;
const extendedClipboardFormatHtml = 1 << 2;
const extendedClipboardFormatDib = 1 << 3;
const extendedClipboardFormatFiles = 1 << 4;
/*eslint-enable */
// Extended clipboard pseudo-encoding actions
const extendedClipboardActionCaps = 1 << 24;
const extendedClipboardActionRequest = 1 << 25;
const extendedClipboardActionPeek = 1 << 26;
const extendedClipboardActionNotify = 1 << 27;
const extendedClipboardActionProvide = 1 << 28;
export default class RFB extends EventTargetMixin {
constructor(target, url, options) {
if (!target) {
throw new Error("Must specify target");
}
if (!url) {
throw new Error("Must specify URL");
}
super();
this._target = target;
this._url = url;
// Connection details
options = options || {};
this._rfbCredentials = options.credentials || {};
this._shared = 'shared' in options ? !!options.shared : true;
this._repeaterID = options.repeaterID || '';
this._wsProtocols = ['binary'];
// Internal state
this._rfbConnectionState = '';
this._rfbInitState = '';
this._rfbAuthScheme = -1;
this._rfbCleanDisconnect = true;
// Server capabilities
this._rfbVersion = 0;
this._rfbMaxVersion = 3.8;
this._rfbTightVNC = false;
this._rfbVeNCryptState = 0;
this._rfbXvpVer = 0;
this._fbWidth = 0;
this._fbHeight = 0;
this._fbName = "";
this._capabilities = { power: false };
this._supportsFence = false;
this._supportsContinuousUpdates = false;
this._enabledContinuousUpdates = false;
this._supportsSetDesktopSize = false;
this._screenID = 0;
this._screenFlags = 0;
this._qemuExtKeyEventSupported = false;
this._clipboardText = null;
this._clipboardServerCapabilitiesActions = {};
this._clipboardServerCapabilitiesFormats = {};
// Internal objects
this._sock = null; // Websock object
this._display = null; // Display object
this._flushing = false; // Display flushing state
this._keyboard = null; // Keyboard input handler object
this._gestures = null; // Gesture input handler object
// Timers
this._disconnTimer = null; // disconnection timer
this._resizeTimeout = null; // resize rate limiting
this._mouseMoveTimer = null;
// Decoder states
this._decoders = {};
this._FBU = {
rects: 0,
x: 0,
y: 0,
width: 0,
height: 0,
encoding: null,
};
// Mouse state
this._mousePos = {};
this._mouseButtonMask = 0;
this._mouseLastMoveTime = 0;
this._viewportDragging = false;
this._viewportDragPos = {};
this._viewportHasMoved = false;
this._accumulatedWheelDeltaX = 0;
this._accumulatedWheelDeltaY = 0;
// Gesture state
this._gestureLastTapTime = null;
this._gestureFirstDoubleTapEv = null;
this._gestureLastMagnitudeX = 0;
this._gestureLastMagnitudeY = 0;
// Bound event handlers
this._eventHandlers = {
focusCanvas: this._focusCanvas.bind(this),
windowResize: this._windowResize.bind(this),
handleMouse: this._handleMouse.bind(this),
handleWheel: this._handleWheel.bind(this),
handleGesture: this._handleGesture.bind(this),
};
// main setup
Log.Debug(">> RFB.constructor");
// Create DOM elements
this._screen = document.createElement('div');
this._screen.style.display = 'flex';
this._screen.style.width = '100%';
this._screen.style.height = '100%';
this._screen.style.overflow = 'auto';
this._screen.style.background = DEFAULT_BACKGROUND;
this._canvas = document.createElement('canvas');
this._canvas.style.margin = 'auto';
// Some browsers add an outline on focus
this._canvas.style.outline = 'none';
// IE miscalculates width without this :(
this._canvas.style.flexShrink = '0';
this._canvas.width = 0;
this._canvas.height = 0;
this._canvas.tabIndex = -1;
this._screen.appendChild(this._canvas);
// Cursor
this._cursor = new Cursor();
// XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
// it. Result: no cursor at all until a window border or an edit field
// is hit blindly. But there are also VNC servers that draw the cursor
// in the framebuffer and don't send the empty local cursor. There is
// no way to satisfy both sides.
//
// The spec is unclear on this "initial cursor" issue. Many other
// viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
// initial cursor instead.
this._cursorImage = RFB.cursors.none;
// populate decoder array with objects
this._decoders[encodings.encodingRaw] = new RawDecoder();
this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
this._decoders[encodings.encodingRRE] = new RREDecoder();
this._decoders[encodings.encodingHextile] = new HextileDecoder();
this._decoders[encodings.encodingTight] = new TightDecoder();
this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
// NB: nothing that needs explicit teardown should be done
// before this point, since this can throw an exception
try {
this._display = new Display(this._canvas);
} catch (exc) {
Log.Error("Display exception: " + exc);
throw exc;
}
this._display.onflush = this._onFlush.bind(this);
this._keyboard = new Keyboard(this._canvas);
this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
this._gestures = new GestureHandler();
this._sock = new Websock();
this._sock.on('message', () => {
this._handleMessage();
});
this._sock.on('open', () => {
if ((this._rfbConnectionState === 'connecting') &&
(this._rfbInitState === '')) {
this._rfbInitState = 'ProtocolVersion';
Log.Debug("Starting VNC handshake");
} else {
this._fail("Unexpected server connection while " +
this._rfbConnectionState);
}
});
this._sock.on('close', (e) => {
Log.Debug("WebSocket on-close event");
let msg = "";
if (e.code) {
msg = "(code: " + e.code;
if (e.reason) {
msg += ", reason: " + e.reason;
}
msg += ")";
}
switch (this._rfbConnectionState) {
case 'connecting':
this._fail("Connection closed " + msg);
break;
case 'connected':
// Handle disconnects that were initiated server-side
this._updateConnectionState('disconnecting');
this._updateConnectionState('disconnected');
break;
case 'disconnecting':
// Normal disconnection path
this._updateConnectionState('disconnected');
break;
case 'disconnected':
this._fail("Unexpected server disconnect " +
"when already disconnected " + msg);
break;
default:
this._fail("Unexpected server disconnect before connecting " +
msg);
break;
}
this._sock.off('close');
});
this._sock.on('error', e => Log.Warn("WebSocket on-error event"));
// Slight delay of the actual connection so that the caller has
// time to set up callbacks
setTimeout(this._updateConnectionState.bind(this, 'connecting'));
Log.Debug("<< RFB.constructor");
// ===== PROPERTIES =====
this.dragViewport = false;
this.focusOnClick = true;
this._viewOnly = false;
this._clipViewport = false;
this._scaleViewport = false;
this._resizeSession = false;
this._showDotCursor = false;
if (options.showDotCursor !== undefined) {
Log.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated");
this._showDotCursor = options.showDotCursor;
}
this._qualityLevel = 6;
this._compressionLevel = 2;
}
// ===== PROPERTIES =====
get viewOnly() { return this._viewOnly; }
set viewOnly(viewOnly) {
this._viewOnly = viewOnly;
if (this._rfbConnectionState === "connecting" ||
this._rfbConnectionState === "connected") {
if (viewOnly) {
this._keyboard.ungrab();
} else {
this._keyboard.grab();
}
}
}
get capabilities() { return this._capabilities; }
get touchButton() { return 0; }
set touchButton(button) { Log.Warn("Using old API!"); }
get clipViewport() { return this._clipViewport; }
set clipViewport(viewport) {
this._clipViewport = viewport;
this._updateClip();
}
get scaleViewport() { return this._scaleViewport; }
set scaleViewport(scale) {
this._scaleViewport = scale;
// Scaling trumps clipping, so we may need to adjust
// clipping when enabling or disabling scaling
if (scale && this._clipViewport) {
this._updateClip();
}
this._updateScale();
if (!scale && this._clipViewport) {
this._updateClip();
}
}
get resizeSession() { return this._resizeSession; }
set resizeSession(resize) {
this._resizeSession = resize;
if (resize) {
this._requestRemoteResize();
}
}
get showDotCursor() { return this._showDotCursor; }
set showDotCursor(show) {
this._showDotCursor = show;
this._refreshCursor();
}
get background() { return this._screen.style.background; }
set background(cssValue) { this._screen.style.background = cssValue; }
get qualityLevel() {
return this._qualityLevel;
}
set qualityLevel(qualityLevel) {
if (!Number.isInteger(qualityLevel) || qualityLevel < 0 || qualityLevel > 9) {
Log.Error("qualityLevel must be an integer between 0 and 9");
return;
}
if (this._qualityLevel === qualityLevel) {
return;
}
this._qualityLevel = qualityLevel;
if (this._rfbConnectionState === 'connected') {
this._sendEncodings();
}
}
get compressionLevel() {
return this._compressionLevel;
}
set compressionLevel(compressionLevel) {
if (!Number.isInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) {
Log.Error("compressionLevel must be an integer between 0 and 9");
return;
}
if (this._compressionLevel === compressionLevel) {
return;
}
this._compressionLevel = compressionLevel;
if (this._rfbConnectionState === 'connected') {
this._sendEncodings();
}
}
// ===== PUBLIC METHODS =====
disconnect() {
this._updateConnectionState('disconnecting');
this._sock.off('error');
this._sock.off('message');
this._sock.off('open');
}
sendCredentials(creds) {
this._rfbCredentials = creds;
setTimeout(this._initMsg.bind(this), 0);
}
sendText(text) {
for (var i = 0; i < text.length; i++) {
const character = text.charAt(i);
var charCode = USKeyTable[character] || false;
if (charCode) {
this.sendKey(charCode, character, true);
this.sendKey(charCode, character, false);
} else {
charCode = text.charCodeAt(i)
this.sendKey(KeyTable.XK_Shift_L, "ShiftLeft", true);
this.sendKey(charCode, character, true);
this.sendKey(charCode, character, false);
this.sendKey(KeyTable.XK_Shift_L, "ShiftLeft", false);
}
}
}
sendCtrlAltDel() {
if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
Log.Info("Sending Ctrl-Alt-Del");
this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
this.sendKey(KeyTable.XK_Delete, "Delete", true);
this.sendKey(KeyTable.XK_Delete, "Delete", false);
this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
}
sendCtrlEsc() {
if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
Log.Info("Sending Ctrl-Esc");
this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
this.sendKey(KeyTable.XK_Escape, "Escape", true);
this.sendKey(KeyTable.XK_Escape, "Escape", false);
this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
}
machineShutdown() {
this._xvpOp(1, 2);
}
machineReboot() {
this._xvpOp(1, 3);
}
machineReset() {
this._xvpOp(1, 4);
}
// Send a key press. If 'down' is not specified then send a down key
// followed by an up key.
sendKey(keysym, code, down) {
if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
if (down === undefined) {
this.sendKey(keysym, code, true);
this.sendKey(keysym, code, false);
return;
}
const scancode = XtScancode[code];
if (this._qemuExtKeyEventSupported && scancode) {
// 0 is NoSymbol
keysym = keysym || 0;
Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
} else {
if (!keysym) {
return;
}
Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
}
}
focus() {
this._canvas.focus();
}
blur() {
this._canvas.blur();
}
clipboardPasteFrom(text) {
if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
if (this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] &&
this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
this._clipboardText = text;
RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
} else {
let data = new Uint8Array(text.length);
for (let i = 0; i < text.length; i++) {
// FIXME: text can have values outside of Latin1/Uint8
data[i] = text.charCodeAt(i);
}
RFB.messages.clientCutText(this._sock, data);
}
}
// ===== PRIVATE METHODS =====
_connect() {
Log.Debug(">> RFB.connect");
Log.Info("connecting to " + this._url);
try {
// WebSocket.onopen transitions to the RFB init states
this._sock.open(this._url, this._wsProtocols);
} catch (e) {
if (e.name === 'SyntaxError') {
this._fail("Invalid host or port (" + e + ")");
} else {
this._fail("Error when opening socket (" + e + ")");
}
}
// Make our elements part of the page
this._target.appendChild(this._screen);
this._gestures.attach(this._canvas);
this._cursor.attach(this._canvas);
this._refreshCursor();
// Monitor size changes of the screen
// FIXME: Use ResizeObserver, or hidden overflow
window.addEventListener('resize', this._eventHandlers.windowResize);
// Always grab focus on some kind of click event
this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);
// Mouse events
this._canvas.addEventListener('mousedown', this._eventHandlers.handleMouse);
this._canvas.addEventListener('mouseup', this._eventHandlers.handleMouse);
this._canvas.addEventListener('mousemove', this._eventHandlers.handleMouse);
// Prevent middle-click pasting (see handler for why we bind to document)
this._canvas.addEventListener('click', this._eventHandlers.handleMouse);
// preventDefault() on mousedown doesn't stop this event for some
// reason so we have to explicitly block it
this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);
// Wheel events
this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);
// Gesture events
this._canvas.addEventListener("gesturestart", this._eventHandlers.handleGesture);
this._canvas.addEventListener("gesturemove", this._eventHandlers.handleGesture);
this._canvas.addEventListener("gestureend", this._eventHandlers.handleGesture);
Log.Debug("<< RFB.connect");
}
_disconnect() {
Log.Debug(">> RFB.disconnect");
this._cursor.detach();
this._canvas.removeEventListener("gesturestart", this._eventHandlers.handleGesture);
this._canvas.removeEventListener("gesturemove", this._eventHandlers.handleGesture);
this._canvas.removeEventListener("gestureend", this._eventHandlers.handleGesture);
this._canvas.removeEventListener("wheel", this._eventHandlers.handleWheel);
this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse);
this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse);
this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);
this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);
this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);
this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
window.removeEventListener('resize', this._eventHandlers.windowResize);
this._keyboard.ungrab();
this._gestures.detach();
this._sock.close();
try {
this._target.removeChild(this._screen);
} catch (e) {
if (e.name === 'NotFoundError') {
// Some cases where the initial connection fails
// can disconnect before the _screen is created
} else {
throw e;
}
}
clearTimeout(this._resizeTimeout);
clearTimeout(this._mouseMoveTimer);
Log.Debug("<< RFB.disconnect");
}
_focusCanvas(event) {
if (!this.focusOnClick) {
return;
}
this.focus();
}
_setDesktopName(name) {
this._fbName = name;
this.dispatchEvent(new CustomEvent(
"desktopname",
{ detail: { name: this._fbName } }));
}
_windowResize(event) {
// If the window resized then our screen element might have
// as well. Update the viewport dimensions.
window.requestAnimationFrame(() => {
this._updateClip();
this._updateScale();
});
if (this._resizeSession) {
// Request changing the resolution of the remote display to
// the size of the local browser viewport.
// In order to not send multiple requests before the browser-resize
// is finished we wait 0.5 seconds before sending the request.
clearTimeout(this._resizeTimeout);
this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500);
}
}
// Update state of clipping in Display object, and make sure the
// configured viewport matches the current screen size
_updateClip() {
const curClip = this._display.clipViewport;
let newClip = this._clipViewport;
if (this._scaleViewport) {
// Disable viewport clipping if we are scaling
newClip = false;
}
if (curClip !== newClip) {
this._display.clipViewport = newClip;
}
if (newClip) {
// When clipping is enabled, the screen is limited to
// the size of the container.
const size = this._screenSize();
this._display.viewportChangeSize(size.w, size.h);
this._fixScrollbars();
}
}
_updateScale() {
if (!this._scaleViewport) {
this._display.scale = 1.0;
} else {
const size = this._screenSize();
this._display.autoscale(size.w, size.h);
}
this._fixScrollbars();
}
// Requests a change of remote desktop size. This message is an extension
// and may only be sent if we have received an ExtendedDesktopSize message
_requestRemoteResize() {
clearTimeout(this._resizeTimeout);
this._resizeTimeout = null;
if (!this._resizeSession || this._viewOnly ||
!this._supportsSetDesktopSize) {
return;
}
const size = this._screenSize();
RFB.messages.setDesktopSize(this._sock,
Math.floor(size.w), Math.floor(size.h),
this._screenID, this._screenFlags);
Log.Debug('Requested new desktop size: ' +
size.w + 'x' + size.h);
}
// Gets the the size of the available screen
_screenSize() {
let r = this._screen.getBoundingClientRect();
return { w: r.width, h: r.height };
}
_fixScrollbars() {
// This is a hack because Chrome screws up the calculation
// for when scrollbars are needed. So to fix it we temporarily
// toggle them off and on.
const orig = this._screen.style.overflow;
this._screen.style.overflow = 'hidden';
// Force Chrome to recalculate the layout by asking for
// an element's dimensions
this._screen.getBoundingClientRect();
this._screen.style.overflow = orig;
}
/*
* Connection states:
* connecting
* connected
* disconnecting
* disconnected - permanent state
*/
_updateConnectionState(state) {
const oldstate = this._rfbConnectionState;
if (state === oldstate) {
Log.Debug("Already in state '" + state + "', ignoring");
return;
}
// The 'disconnected' state is permanent for each RFB object
if (oldstate === 'disconnected') {
Log.Error("Tried changing state of a disconnected RFB object");
return;
}
// Ensure proper transitions before doing anything
switch (state) {
case 'connected':
if (oldstate !== 'connecting') {
Log.Error("Bad transition to connected state, " +
"previous connection state: " + oldstate);
return;
}
break;
case 'disconnected':
if (oldstate !== 'disconnecting') {
Log.Error("Bad transition to disconnected state, " +
"previous connection state: " + oldstate);
return;
}
break;
case 'connecting':
if (oldstate !== '') {
Log.Error("Bad transition to connecting state, " +
"previous connection state: " + oldstate);
return;
}
break;
case 'disconnecting':
if (oldstate !== 'connected' && oldstate !== 'connecting') {
Log.Error("Bad transition to disconnecting state, " +
"previous connection state: " + oldstate);
return;
}
break;
default:
Log.Error("Unknown connection state: " + state);
return;
}
// State change actions
this._rfbConnectionState = state;
Log.Debug("New state '" + state + "', was '" + oldstate + "'.");
if (this._disconnTimer && state !== 'disconnecting') {
Log.Debug("Clearing disconnect timer");
clearTimeout(this._disconnTimer);
this._disconnTimer = null;
// make sure we don't get a double event
this._sock.off('close');
}
switch (state) {
case 'connecting':
this._connect();
break;
case 'connected':
this.dispatchEvent(new CustomEvent("connect", { detail: {} }));
break;
case 'disconnecting':
this._disconnect();
this._disconnTimer = setTimeout(() => {
Log.Error("Disconnection timed out.");
this._updateConnectionState('disconnected');
}, DISCONNECT_TIMEOUT * 1000);
break;
case 'disconnected':
this.dispatchEvent(new CustomEvent(
"disconnect", { detail:
{ clean: this._rfbCleanDisconnect } }));
break;
}
}
/* Print errors and disconnect
*
* The parameter 'details' is used for information that
* should be logged but not sent to the user interface.
*/
_fail(details) {
switch (this._rfbConnectionState) {
case 'disconnecting':
Log.Error("Failed when disconnecting: " + details);
break;
case 'connected':
Log.Error("Failed while connected: " + details);
break;
case 'connecting':
Log.Error("Failed when connecting: " + details);
break;
default:
Log.Error("RFB failure: " + details);
break;
}
this._rfbCleanDisconnect = false; //This is sent to the UI
// Transition to disconnected without waiting for socket to close
this._updateConnectionState('disconnecting');
this._updateConnectionState('disconnected');
return false;
}
_setCapability(cap, val) {
this._capabilities[cap] = val;
this.dispatchEvent(new CustomEvent("capabilities",
{ detail: { capabilities: this._capabilities } }));
}
_handleMessage() {
if (this._sock.rQlen === 0) {
Log.Warn("handleMessage called on an empty receive queue");
return;
}
switch (this._rfbConnectionState) {
case 'disconnected':
Log.Error("Got data while disconnected");
break;
case 'connected':
while (true) {
if (this._flushing) {
break;
}
if (!this._normalMsg()) {
break;
}
if (this._sock.rQlen === 0) {
break;
}
}
break;
default:
this._initMsg();
break;
}
}
_handleKeyEvent(keysym, code, down) {
this.sendKey(keysym, code, down);
}
_handleMouse(ev) {
/*
* We don't check connection status or viewOnly here as the
* mouse events might be used to control the viewport
*/
if (ev.type === 'click') {
/*
* Note: This is only needed for the 'click' event as it fails
* to fire properly for the target element so we have
* to listen on the document element instead.
*/
if (ev.target !== this._canvas) {
return;
}
}
// FIXME: if we're in view-only and not dragging,
// should we stop events?
ev.stopPropagation();
ev.preventDefault();
if ((ev.type === 'click') || (ev.type === 'contextmenu')) {
return;
}
let pos = clientToElement(ev.clientX, ev.clientY,
this._canvas);
switch (ev.type) {
case 'mousedown':
setCapture(this._canvas);
this._handleMouseButton(pos.x, pos.y,
true, 1 << ev.button);
break;
case 'mouseup':
this._handleMouseButton(pos.x, pos.y,
false, 1 << ev.button);
break;
case 'mousemove':
this._handleMouseMove(pos.x, pos.y);
break;
}
}
_handleMouseButton(x, y, down, bmask) {
if (this.dragViewport) {
if (down && !this._viewportDragging) {
this._viewportDragging = true;
this._viewportDragPos = {'x': x, 'y': y};
this._viewportHasMoved = false;
// Skip sending mouse events
return;
} else {
this._viewportDragging = false;
// If we actually performed a drag then we are done
// here and should not send any mouse events
if (this._viewportHasMoved) {
return;
}
// Otherwise we treat this as a mouse click event.
// Send the button down event here, as the button up
// event is sent at the end of this function.
this._sendMouse(x, y, bmask);
}
}
// Flush waiting move event first
if (this._mouseMoveTimer !== null) {
clearTimeout(this._mouseMoveTimer);
this._mouseMoveTimer = null;
this._sendMouse(x, y, this._mouseButtonMask);
}
if (down) {
this._mouseButtonMask |= bmask;
} else {
this._mouseButtonMask &= ~bmask;
}
this._sendMouse(x, y, this._mouseButtonMask);
}
_handleMouseMove(x, y) {
if (this._viewportDragging) {
const deltaX = this._viewportDragPos.x - x;
const deltaY = this._viewportDragPos.y - y;
if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
Math.abs(deltaY) > dragThreshold)) {
this._viewportHasMoved = true;
this._viewportDragPos = {'x': x, 'y': y};
this._display.viewportChangePos(deltaX, deltaY);
}
// Skip sending mouse events
return;
}
this._mousePos = { 'x': x, 'y': y };
// Limit many mouse move events to one every MOUSE_MOVE_DELAY ms
if (this._mouseMoveTimer == null) {
const timeSinceLastMove = Date.now() - this._mouseLastMoveTime;
if (timeSinceLastMove > MOUSE_MOVE_DELAY) {
this._sendMouse(x, y, this._mouseButtonMask);
this._mouseLastMoveTime = Date.now();
} else {
// Too soon since the latest move, wait the remaining time
this._mouseMoveTimer = setTimeout(() => {
this._handleDelayedMouseMove();
}, MOUSE_MOVE_DELAY - timeSinceLastMove);
}
}
}
_handleDelayedMouseMove() {
this._mouseMoveTimer = null;
this._sendMouse(this._mousePos.x, this._mousePos.y,
this._mouseButtonMask);
this._mouseLastMoveTime = Date.now();
}
_sendMouse(x, y, mask) {
if (this._rfbConnectionState !== 'connected') { return; }
if (this._viewOnly) { return; } // View only, skip mouse events
RFB.messages.pointerEvent(this._sock, this._display.absX(x),
this._display.absY(y), mask);
}
_handleWheel(ev) {
if (this._rfbConnectionState !== 'connected') { return; }
if (this._viewOnly) { return; } // View only, skip mouse events
ev.stopPropagation();
ev.preventDefault();
let pos = clientToElement(ev.clientX, ev.clientY,
this._canvas);
let dX = ev.deltaX;
let dY = ev.deltaY;
// Pixel units unless it's non-zero.
// Note that if deltamode is line or page won't matter since we aren't
// sending the mouse wheel delta to the server anyway.
// The difference between pixel and line can be important however since
// we have a threshold that can be smaller than the line height.
if (ev.deltaMode !== 0) {
dX *= WHEEL_LINE_HEIGHT;
dY *= WHEEL_LINE_HEIGHT;
}
// Mouse wheel events are sent in steps over VNC. This means that the VNC
// protocol can't handle a wheel event with specific distance or speed.
// Therefor, if we get a lot of small mouse wheel events we combine them.
this._accumulatedWheelDeltaX += dX;
this._accumulatedWheelDeltaY += dY;
// Generate a mouse wheel step event when the accumulated delta
// for one of the axes is large enough.
if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) {
if (this._accumulatedWheelDeltaX < 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 5);
this._handleMouseButton(pos.x, pos.y, false, 1 << 5);
} else if (this._accumulatedWheelDeltaX > 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 6);
this._handleMouseButton(pos.x, pos.y, false, 1 << 6);
}
this._accumulatedWheelDeltaX = 0;
}
if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) {
if (this._accumulatedWheelDeltaY < 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 3);
this._handleMouseButton(pos.x, pos.y, false, 1 << 3);
} else if (this._accumulatedWheelDeltaY > 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 4);
this._handleMouseButton(pos.x, pos.y, false, 1 << 4);
}
this._accumulatedWheelDeltaY = 0;
}
}
_fakeMouseMove(ev, elementX, elementY) {
this._handleMouseMove(elementX, elementY);
this._cursor.move(ev.detail.clientX, ev.detail.clientY);
}
_handleTapEvent(ev, bmask) {
let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
this._canvas);
// If the user quickly taps multiple times we assume they meant to
// hit the same spot, so slightly adjust coordinates
if ((this._gestureLastTapTime !== null) &&
((Date.now() - this._gestureLastTapTime) < DOUBLE_TAP_TIMEOUT) &&
(this._gestureFirstDoubleTapEv.detail.type === ev.detail.type)) {
let dx = this._gestureFirstDoubleTapEv.detail.clientX - ev.detail.clientX;
let dy = this._gestureFirstDoubleTapEv.detail.clientY - ev.detail.clientY;
let distance = Math.hypot(dx, dy);
if (distance < DOUBLE_TAP_THRESHOLD) {
pos = clientToElement(this._gestureFirstDoubleTapEv.detail.clientX,
this._gestureFirstDoubleTapEv.detail.clientY,
this._canvas);
} else {
this._gestureFirstDoubleTapEv = ev;
}
} else {
this._gestureFirstDoubleTapEv = ev;
}
this._gestureLastTapTime = Date.now();
this._fakeMouseMove(this._gestureFirstDoubleTapEv, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, true, bmask);
this._handleMouseButton(pos.x, pos.y, false, bmask);
}
_handleGesture(ev) {
let magnitude;
let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
this._canvas);
switch (ev.type) {
case 'gesturestart':
switch (ev.detail.type) {
case 'onetap':
this._handleTapEvent(ev, 0x1);
break;
case 'twotap':
this._handleTapEvent(ev, 0x4);
break;
case 'threetap':
this._handleTapEvent(ev, 0x2);
break;
case 'drag':
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, true, 0x1);
break;
case 'longpress':
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, true, 0x4);
break;
case 'twodrag':
this._gestureLastMagnitudeX = ev.detail.magnitudeX;
this._gestureLastMagnitudeY = ev.detail.magnitudeY;
this._fakeMouseMove(ev, pos.x, pos.y);
break;
case 'pinch':
this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX,
ev.detail.magnitudeY);
this._fakeMouseMove(ev, pos.x, pos.y);
break;
}
break;
case 'gesturemove':
switch (ev.detail.type) {
case 'onetap':
case 'twotap':
case 'threetap':
break;
case 'drag':
case 'longpress':
this._fakeMouseMove(ev, pos.x, pos.y);
break;
case 'twodrag':
// Always scroll in the same position.
// We don't know if the mouse was moved so we need to move it
// every update.
this._fakeMouseMove(ev, pos.x, pos.y);
while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x8);
this._handleMouseButton(pos.x, pos.y, false, 0x8);
this._gestureLastMagnitudeY += GESTURE_SCRLSENS;
}
while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) < -GESTURE_SCRLSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x10);
this._handleMouseButton(pos.x, pos.y, false, 0x10);
this._gestureLastMagnitudeY -= GESTURE_SCRLSENS;
}
while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) > GESTURE_SCRLSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x20);
this._handleMouseButton(pos.x, pos.y, false, 0x20);
this._gestureLastMagnitudeX += GESTURE_SCRLSENS;
}
while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) < -GESTURE_SCRLSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x40);
this._handleMouseButton(pos.x, pos.y, false, 0x40);
this._gestureLastMagnitudeX -= GESTURE_SCRLSENS;
}
break;
case 'pinch':
// Always scroll in the same position.
// We don't know if the mouse was moved so we need to move it
// every update.
this._fakeMouseMove(ev, pos.x, pos.y);
magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY);
if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x8);
this._handleMouseButton(pos.x, pos.y, false, 0x8);
this._gestureLastMagnitudeX += GESTURE_ZOOMSENS;
}
while ((magnitude - this._gestureLastMagnitudeX) < -GESTURE_ZOOMSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x10);
this._handleMouseButton(pos.x, pos.y, false, 0x10);
this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS;
}
}
this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", false);
break;
}
break;
case 'gestureend':
switch (ev.detail.type) {
case 'onetap':
case 'twotap':
case 'threetap':
case 'pinch':
case 'twodrag':
break;
case 'drag':
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, false, 0x1);
break;
case 'longpress':
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, false, 0x4);
break;
}
break;
}
}
// Message Handlers
_negotiateProtocolVersion() {
if (this._sock.rQwait("version", 12)) {
return false;
}
const sversion = this._sock.rQshiftStr(12).substr(4, 7);
Log.Info("Server ProtocolVersion: " + sversion);
let isRepeater = 0;
switch (sversion) {
case "000.000": // UltraVNC repeater
isRepeater = 1;
break;
case "003.003":
case "003.006": // UltraVNC
case "003.889": // Apple Remote Desktop
this._rfbVersion = 3.3;
break;
case "003.007":
this._rfbVersion = 3.7;
break;
case "003.008":
case "004.000": // Intel AMT KVM
case "004.001": // RealVNC 4.6
case "005.000": // RealVNC 5.3
this._rfbVersion = 3.8;
break;
default:
return this._fail("Invalid server version " + sversion);
}
if (isRepeater) {
let repeaterID = "ID:" + this._repeaterID;
while (repeaterID.length < 250) {
repeaterID += "\0";
}
this._sock.sendString(repeaterID);
return true;
}
if (this._rfbVersion > this._rfbMaxVersion) {
this._rfbVersion = this._rfbMaxVersion;
}
const cversion = "00" + parseInt(this._rfbVersion, 10) +
".00" + ((this._rfbVersion * 10) % 10);
this._sock.sendString("RFB " + cversion + "\n");
Log.Debug('Sent ProtocolVersion: ' + cversion);
this._rfbInitState = 'Security';
}
_negotiateSecurity() {
// Polyfill since IE and PhantomJS doesn't have
// TypedArray.includes()
function includes(item, array) {
for (let i = 0; i < array.length; i++) {
if (array[i] === item) {
return true;
}
}
return false;
}
if (this._rfbVersion >= 3.7) {
// Server sends supported list, client decides
const numTypes = this._sock.rQshift8();
if (this._sock.rQwait("security type", numTypes, 1)) { return false; }
if (numTypes === 0) {
this._rfbInitState = "SecurityReason";
this._securityContext = "no security types";
this._securityStatus = 1;
return this._initMsg();
}
const types = this._sock.rQshiftBytes(numTypes);
Log.Debug("Server security types: " + types);
// Look for each auth in preferred order
if (includes(1, types)) {
this._rfbAuthScheme = 1; // None
} else if (includes(22, types)) {
this._rfbAuthScheme = 22; // XVP
} else if (includes(16, types)) {
this._rfbAuthScheme = 16; // Tight
} else if (includes(2, types)) {
this._rfbAuthScheme = 2; // VNC Auth
} else if (includes(19, types)) {
this._rfbAuthScheme = 19; // VeNCrypt Auth
} else {
return this._fail("Unsupported security types (types: " + types + ")");
}
this._sock.send([this._rfbAuthScheme]);
} else {
// Server decides
if (this._sock.rQwait("security scheme", 4)) { return false; }
this._rfbAuthScheme = this._sock.rQshift32();
if (this._rfbAuthScheme == 0) {
this._rfbInitState = "SecurityReason";
this._securityContext = "authentication scheme";
this._securityStatus = 1;
return this._initMsg();
}
}
this._rfbInitState = 'Authentication';
Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
return this._initMsg(); // jump to authentication
}
_handleSecurityReason() {
if (this._sock.rQwait("reason length", 4)) {
return false;
}
const strlen = this._sock.rQshift32();
let reason = "";
if (strlen > 0) {
if (this._sock.rQwait("reason", strlen, 4)) { return false; }
reason = this._sock.rQshiftStr(strlen);
}
if (reason !== "") {
this.dispatchEvent(new CustomEvent(
"securityfailure",
{ detail: { status: this._securityStatus,
reason: reason } }));
return this._fail("Security negotiation failed on " +
this._securityContext +
" (reason: " + reason + ")");
} else {
this.dispatchEvent(new CustomEvent(
"securityfailure",
{ detail: { status: this._securityStatus } }));
return this._fail("Security negotiation failed on " +
this._securityContext);
}
}
// authentication
_negotiateXvpAuth() {
if (this._rfbCredentials.username === undefined ||
this._rfbCredentials.password === undefined ||
this._rfbCredentials.target === undefined) {
this.dispatchEvent(new CustomEvent(
"credentialsrequired",
{ detail: { types: ["username", "password", "target"] } }));
return false;
}
const xvpAuthStr = String.fromCharCode(this._rfbCredentials.username.length) +
String.fromCharCode(this._rfbCredentials.target.length) +
this._rfbCredentials.username +
this._rfbCredentials.target;
this._sock.sendString(xvpAuthStr);
this._rfbAuthScheme = 2;
return this._negotiateAuthentication();
}
// VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
_negotiateVeNCryptAuth() {
// waiting for VeNCrypt version
if (this._rfbVeNCryptState == 0) {
if (this._sock.rQwait("vencrypt version", 2)) { return false; }
const major = this._sock.rQshift8();
const minor = this._sock.rQshift8();
if (!(major == 0 && minor == 2)) {
return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
}
this._sock.send([0, 2]);
this._rfbVeNCryptState = 1;
}
// waiting for ACK
if (this._rfbVeNCryptState == 1) {
if (this._sock.rQwait("vencrypt ack", 1)) { return false; }
const res = this._sock.rQshift8();
if (res != 0) {
return this._fail("VeNCrypt failure " + res);
}
this._rfbVeNCryptState = 2;
}
// must fall through here (i.e. no "else if"), beacause we may have already received
// the subtypes length and won't be called again
if (this._rfbVeNCryptState == 2) { // waiting for subtypes length
if (this._sock.rQwait("vencrypt subtypes length", 1)) { return false; }
const subtypesLength = this._sock.rQshift8();
if (subtypesLength < 1) {
return this._fail("VeNCrypt subtypes empty");
}
this._rfbVeNCryptSubtypesLength = subtypesLength;
this._rfbVeNCryptState = 3;
}
// waiting for subtypes list
if (this._rfbVeNCryptState == 3) {
if (this._sock.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength)) { return false; }
const subtypes = [];
for (let i = 0; i < this._rfbVeNCryptSubtypesLength; i++) {
subtypes.push(this._sock.rQshift32());
}
// 256 = Plain subtype
if (subtypes.indexOf(256) != -1) {
// 0x100 = 256
this._sock.send([0, 0, 1, 0]);
this._rfbVeNCryptState = 4;
} else {
return this._fail("VeNCrypt Plain subtype not offered by server");
}
}
// negotiated Plain subtype, server waits for password
if (this._rfbVeNCryptState == 4) {
if (!this._rfbCredentials.username ||
!this._rfbCredentials.password) {
this.dispatchEvent(new CustomEvent(
"credentialsrequired",
{ detail: { types: ["username", "password"] } }));
return false;
}
const user = encodeUTF8(this._rfbCredentials.username);
const pass = encodeUTF8(this._rfbCredentials.password);
// XXX we assume lengths are <= 255 (should not be an issue in the real world)
this._sock.send([0, 0, 0, user.length]);
this._sock.send([0, 0, 0, pass.length]);
this._sock.sendString(user);
this._sock.sendString(pass);
this._rfbInitState = "SecurityResult";
return true;
}
}
_negotiateStdVNCAuth() {
if (this._sock.rQwait("auth challenge", 16)) { return false; }
if (this._rfbCredentials.password === undefined) {
this.dispatchEvent(new CustomEvent(
"credentialsrequired",
{ detail: { types: ["password"] } }));
return false;
}
// TODO(directxman12): make genDES not require an Array
const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
const response = RFB.genDES(this._rfbCredentials.password, challenge);
this._sock.send(response);
this._rfbInitState = "SecurityResult";
return true;
}
_negotiateTightUnixAuth() {
if (this._rfbCredentials.username === undefined ||
this._rfbCredentials.password === undefined) {
this.dispatchEvent(new CustomEvent(
"credentialsrequired",
{ detail: { types: ["username", "password"] } }));
return false;
}
this._sock.send([0, 0, 0, this._rfbCredentials.username.length]);
this._sock.send([0, 0, 0, this._rfbCredentials.password.length]);
this._sock.sendString(this._rfbCredentials.username);
this._sock.sendString(this._rfbCredentials.password);
this._rfbInitState = "SecurityResult";
return true;
}
_negotiateTightTunnels(numTunnels) {
const clientSupportedTunnelTypes = {
0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
};
const serverSupportedTunnelTypes = {};
// receive tunnel capabilities
for (let i = 0; i < numTunnels; i++) {
const capCode = this._sock.rQshift32();
const capVendor = this._sock.rQshiftStr(4);
const capSignature = this._sock.rQshiftStr(8);
serverSupportedTunnelTypes[capCode] = { vendor: capVendor, signature: capSignature };
}
Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes);
// Siemens touch panels have a VNC server that supports NOTUNNEL,
// but forgets to advertise it. Try to detect such servers by
// looking for their custom tunnel type.
if (serverSupportedTunnelTypes[1] &&
(serverSupportedTunnelTypes[1].vendor === "SICR") &&
(serverSupportedTunnelTypes[1].signature === "SCHANNEL")) {
Log.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };
}
// choose the notunnel type
if (serverSupportedTunnelTypes[0]) {
if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
return this._fail("Client's tunnel type had the incorrect " +
"vendor or signature");
}
Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
this._sock.send([0, 0, 0, 0]); // use NOTUNNEL
return false; // wait until we receive the sub auth count to continue
} else {
return this._fail("Server wanted tunnels, but doesn't support " +
"the notunnel type");
}
}
_negotiateTightAuth() {
if (!this._rfbTightVNC) { // first pass, do the tunnel negotiation
if (this._sock.rQwait("num tunnels", 4)) { return false; }
const numTunnels = this._sock.rQshift32();
if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
this._rfbTightVNC = true;
if (numTunnels > 0) {
this._negotiateTightTunnels(numTunnels);
return false; // wait until we receive the sub auth to continue
}
}
// second pass, do the sub-auth negotiation
if (this._sock.rQwait("sub auth count", 4)) { return false; }
const subAuthCount = this._sock.rQshift32();
if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected
this._rfbInitState = 'SecurityResult';
return true;
}
if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
const clientSupportedTypes = {
'STDVNOAUTH__': 1,
'STDVVNCAUTH_': 2,
'TGHTULGNAUTH': 129
};
const serverSupportedTypes = [];
for (let i = 0; i < subAuthCount; i++) {
this._sock.rQshift32(); // capNum
const capabilities = this._sock.rQshiftStr(12);
serverSupportedTypes.push(capabilities);
}
Log.Debug("Server Tight authentication types: " + serverSupportedTypes);
for (let authType in clientSupportedTypes) {
if (serverSupportedTypes.indexOf(authType) != -1) {
this._sock.send([0, 0, 0, clientSupportedTypes[authType]]);
Log.Debug("Selected authentication type: " + authType);
switch (authType) {
case 'STDVNOAUTH__': // no auth
this._rfbInitState = 'SecurityResult';
return true;
case 'STDVVNCAUTH_': // VNC auth
this._rfbAuthScheme = 2;
return this._initMsg();
case 'TGHTULGNAUTH': // UNIX auth
this._rfbAuthScheme = 129;
return this._initMsg();
default:
return this._fail("Unsupported tiny auth scheme " +
"(scheme: " + authType + ")");
}
}
}
return this._fail("No supported sub-auth types!");
}
_negotiateAuthentication() {
switch (this._rfbAuthScheme) {
// Let CloudStack handle the authentication (RFB 3.8 requires the client to select the auth scheme)
case 1: // no auth
case 2: // VNC authentication
case 19: // VeNCrypt Security Type
if (this._rfbVersion >= 3.8) {
this._rfbInitState = 'SecurityResult';
return true;
}
this._rfbInitState = 'ClientInitialisation';
return this._initMsg();
case 22: // XVP auth
return this._negotiateXvpAuth();
case 16: // TightVNC Security Type
return this._negotiateTightAuth();
case 129: // TightVNC UNIX Security Type
return this._negotiateTightUnixAuth();
default:
return this._fail("Unsupported auth scheme (scheme: " +
this._rfbAuthScheme + ")");
}
}
_handleSecurityResult() {
if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
const status = this._sock.rQshift32();
if (status === 0) { // OK
this._rfbInitState = 'ClientInitialisation';
Log.Debug('Authentication OK');
return this._initMsg();
} else {
if (this._rfbVersion >= 3.8) {
this._rfbInitState = "SecurityReason";
this._securityContext = "security result";
this._securityStatus = status;
return this._initMsg();
} else {
this.dispatchEvent(new CustomEvent(
"securityfailure",
{ detail: { status: status } }));
return this._fail("Security handshake failed");
}
}
}
_negotiateServerInit() {
if (this._sock.rQwait("server initialization", 24)) { return false; }
/* Screen size */
const width = this._sock.rQshift16();
const height = this._sock.rQshift16();
/* PIXEL_FORMAT */
const bpp = this._sock.rQshift8();
const depth = this._sock.rQshift8();
const bigEndian = this._sock.rQshift8();
const trueColor = this._sock.rQshift8();
const redMax = this._sock.rQshift16();
const greenMax = this._sock.rQshift16();
const blueMax = this._sock.rQshift16();
const redShift = this._sock.rQshift8();
const greenShift = this._sock.rQshift8();
const blueShift = this._sock.rQshift8();
this._sock.rQskipBytes(3); // padding
// NB(directxman12): we don't want to call any callbacks or print messages until
// *after* we're past the point where we could backtrack
/* Connection name/title */
const nameLength = this._sock.rQshift32();
if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
let name = this._sock.rQshiftStr(nameLength);
name = decodeUTF8(name, true);
if (this._rfbTightVNC) {
if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
// In TightVNC mode, ServerInit message is extended
const numServerMessages = this._sock.rQshift16();
const numClientMessages = this._sock.rQshift16();
const numEncodings = this._sock.rQshift16();
this._sock.rQskipBytes(2); // padding
const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
// we don't actually do anything with the capability information that TIGHT sends,
// so we just skip the all of this.
// TIGHT server message capabilities
this._sock.rQskipBytes(16 * numServerMessages);
// TIGHT client message capabilities
this._sock.rQskipBytes(16 * numClientMessages);
// TIGHT encoding capabilities
this._sock.rQskipBytes(16 * numEncodings);
}
// NB(directxman12): these are down here so that we don't run them multiple times
// if we backtrack
Log.Info("Screen: " + width + "x" + height +
", bpp: " + bpp + ", depth: " + depth +
", bigEndian: " + bigEndian +
", trueColor: " + trueColor +
", redMax: " + redMax +
", greenMax: " + greenMax +
", blueMax: " + blueMax +
", redShift: " + redShift +
", greenShift: " + greenShift +
", blueShift: " + blueShift);
// we're past the point where we could backtrack, so it's safe to call this
this._setDesktopName(name);
this._resize(width, height);
if (!this._viewOnly) { this._keyboard.grab(); }
this._fbDepth = 24;
if (this._fbName === "Intel(r) AMT KVM") {
Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
this._fbDepth = 8;
}
RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
this._sendEncodings();
RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
this._updateConnectionState('connected');
return true;
}
_sendEncodings() {
const encs = [];
// In preference order
encs.push(encodings.encodingCopyRect);
// Only supported with full depth support
if (this._fbDepth == 24) {
encs.push(encodings.encodingTight);
encs.push(encodings.encodingTightPNG);
encs.push(encodings.encodingHextile);
encs.push(encodings.encodingRRE);
}
encs.push(encodings.encodingRaw);
// Psuedo-encoding settings
encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
encs.push(encodings.pseudoEncodingDesktopSize);
encs.push(encodings.pseudoEncodingLastRect);
encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
encs.push(encodings.pseudoEncodingExtendedDesktopSize);
encs.push(encodings.pseudoEncodingXvp);
encs.push(encodings.pseudoEncodingFence);
encs.push(encodings.pseudoEncodingContinuousUpdates);
encs.push(encodings.pseudoEncodingDesktopName);
encs.push(encodings.pseudoEncodingExtendedClipboard);
if (this._fbDepth == 24) {
encs.push(encodings.pseudoEncodingVMwareCursor);
encs.push(encodings.pseudoEncodingCursor);
}
RFB.messages.clientEncodings(this._sock, encs);
}
/* RFB protocol initialization states:
* ProtocolVersion
* Security
* Authentication
* SecurityResult
* ClientInitialization - not triggered by server message
* ServerInitialization
*/
_initMsg() {
switch (this._rfbInitState) {
case 'ProtocolVersion':
return this._negotiateProtocolVersion();
case 'Security':
return this._negotiateSecurity();
case 'Authentication':
return this._negotiateAuthentication();
case 'SecurityResult':
return this._handleSecurityResult();
case 'SecurityReason':
return this._handleSecurityReason();
case 'ClientInitialisation':
this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
this._rfbInitState = 'ServerInitialisation';
return true;
case 'ServerInitialisation':
return this._negotiateServerInit();
default:
return this._fail("Unknown init state (state: " +
this._rfbInitState + ")");
}
}
_handleSetColourMapMsg() {
Log.Debug("SetColorMapEntries");
return this._fail("Unexpected SetColorMapEntries message");
}
_handleServerCutText() {
Log.Debug("ServerCutText");
if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
this._sock.rQskipBytes(3); // Padding
let length = this._sock.rQshift32();
length = toSigned32bit(length);
if (this._sock.rQwait("ServerCutText content", Math.abs(length), 8)) { return false; }
if (length >= 0) {
//Standard msg
const text = this._sock.rQshiftStr(length);
if (this._viewOnly) {
return true;
}
this.dispatchEvent(new CustomEvent(
"clipboard",
{ detail: { text: text } }));
} else {
//Extended msg.
length = Math.abs(length);
const flags = this._sock.rQshift32();
let formats = flags & 0x0000FFFF;
let actions = flags & 0xFF000000;
let isCaps = (!!(actions & extendedClipboardActionCaps));
if (isCaps) {
this._clipboardServerCapabilitiesFormats = {};
this._clipboardServerCapabilitiesActions = {};
// Update our server capabilities for Formats
for (let i = 0; i <= 15; i++) {
let index = 1 << i;
// Check if format flag is set.
if ((formats & index)) {
this._clipboardServerCapabilitiesFormats[index] = true;
// We don't send unsolicited clipboard, so we
// ignore the size
this._sock.rQshift32();
}
}
// Update our server capabilities for Actions
for (let i = 24; i <= 31; i++) {
let index = 1 << i;
this._clipboardServerCapabilitiesActions[index] = !!(actions & index);
}
/* Caps handling done, send caps with the clients
capabilities set as a response */
let clientActions = [
extendedClipboardActionCaps,
extendedClipboardActionRequest,
extendedClipboardActionPeek,
extendedClipboardActionNotify,
extendedClipboardActionProvide
];
RFB.messages.extendedClipboardCaps(this._sock, clientActions, {extendedClipboardFormatText: 0});
} else if (actions === extendedClipboardActionRequest) {
if (this._viewOnly) {
return true;
}
// Check if server has told us it can handle Provide and there is clipboard data to send.
if (this._clipboardText != null &&
this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) {
if (formats & extendedClipboardFormatText) {
RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]);
}
}
} else if (actions === extendedClipboardActionPeek) {
if (this._viewOnly) {
return true;
}
if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
if (this._clipboardText != null) {
RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
} else {
RFB.messages.extendedClipboardNotify(this._sock, []);
}
}
} else if (actions === extendedClipboardActionNotify) {
if (this._viewOnly) {
return true;
}
if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) {
if (formats & extendedClipboardFormatText) {
RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]);
}
}
} else if (actions === extendedClipboardActionProvide) {
if (this._viewOnly) {
return true;
}
if (!(formats & extendedClipboardFormatText)) {
return true;
}
// Ignore what we had in our clipboard client side.
this._clipboardText = null;
// FIXME: Should probably verify that this data was actually requested
let zlibStream = this._sock.rQshiftBytes(length - 4);
let streamInflator = new Inflator();
let textData = null;
streamInflator.setInput(zlibStream);
for (let i = 0; i <= 15; i++) {
let format = 1 << i;
if (formats & format) {
let size = 0x00;
let sizeArray = streamInflator.inflate(4);
size |= (sizeArray[0] << 24);
size |= (sizeArray[1] << 16);
size |= (sizeArray[2] << 8);
size |= (sizeArray[3]);
let chunk = streamInflator.inflate(size);
if (format === extendedClipboardFormatText) {
textData = chunk;
}
}
}
streamInflator.setInput(null);
if (textData !== null) {
let tmpText = "";
for (let i = 0; i < textData.length; i++) {
tmpText += String.fromCharCode(textData[i]);
}
textData = tmpText;
textData = decodeUTF8(textData);
if ((textData.length > 0) && "\0" === textData.charAt(textData.length - 1)) {
textData = textData.slice(0, -1);
}
textData = textData.replace("\r\n", "\n");
this.dispatchEvent(new CustomEvent(
"clipboard",
{ detail: { text: textData } }));
}
} else {
return this._fail("Unexpected action in extended clipboard message: " + actions);
}
}
return true;
}
_handleServerFenceMsg() {
if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; }
this._sock.rQskipBytes(3); // Padding
let flags = this._sock.rQshift32();
let length = this._sock.rQshift8();
if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; }
if (length > 64) {
Log.Warn("Bad payload length (" + length + ") in fence response");
length = 64;
}
const payload = this._sock.rQshiftStr(length);
this._supportsFence = true;
/*
* Fence flags
*
* (1<<0) - BlockBefore
* (1<<1) - BlockAfter
* (1<<2) - SyncNext
* (1<<31) - Request
*/
if (!(flags & (1<<31))) {
return this._fail("Unexpected fence response");
}
// Filter out unsupported flags
// FIXME: support syncNext
flags &= (1<<0) | (1<<1);
// BlockBefore and BlockAfter are automatically handled by
// the fact that we process each incoming message
// synchronuosly.
RFB.messages.clientFence(this._sock, flags, payload);
return true;
}
_handleXvpMsg() {
if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
this._sock.rQskipBytes(1); // Padding
const xvpVer = this._sock.rQshift8();
const xvpMsg = this._sock.rQshift8();
switch (xvpMsg) {
case 0: // XVP_FAIL
Log.Error("XVP Operation Failed");
break;
case 1: // XVP_INIT
this._rfbXvpVer = xvpVer;
Log.Info("XVP extensions enabled (version " + this._rfbXvpVer + ")");
this._setCapability("power", true);
break;
default:
this._fail("Illegal server XVP message (msg: " + xvpMsg + ")");
break;
}
return true;
}
_normalMsg() {
let msgType;
if (this._FBU.rects > 0) {
msgType = 0;
} else {
msgType = this._sock.rQshift8();
}
let first, ret;
switch (msgType) {
case 0: // FramebufferUpdate
ret = this._framebufferUpdate();
if (ret && !this._enabledContinuousUpdates) {
RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,
this._fbWidth, this._fbHeight);
}
return ret;
case 1: // SetColorMapEntries
return this._handleSetColourMapMsg();
case 2: // Bell
Log.Debug("Bell");
this.dispatchEvent(new CustomEvent(
"bell",
{ detail: {} }));
return true;
case 3: // ServerCutText
return this._handleServerCutText();
case 150: // EndOfContinuousUpdates
first = !this._supportsContinuousUpdates;
this._supportsContinuousUpdates = true;
this._enabledContinuousUpdates = false;
if (first) {
this._enabledContinuousUpdates = true;
this._updateContinuousUpdates();
Log.Info("Enabling continuous updates.");
} else {
// FIXME: We need to send a framebufferupdaterequest here
// if we add support for turning off continuous updates
}
return true;
case 248: // ServerFence
return this._handleServerFenceMsg();
case 250: // XVP
return this._handleXvpMsg();
default:
this._fail("Unexpected server message (type " + msgType + ")");
Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
return true;
}
}
_onFlush() {
this._flushing = false;
// Resume processing
if (this._sock.rQlen > 0) {
this._handleMessage();
}
}
_framebufferUpdate() {
if (this._FBU.rects === 0) {
if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
this._sock.rQskipBytes(1); // Padding
this._FBU.rects = this._sock.rQshift16();
// Make sure the previous frame is fully rendered first
// to avoid building up an excessive queue
if (this._display.pending()) {
this._flushing = true;
this._display.flush();
return false;
}
}
while (this._FBU.rects > 0) {
if (this._FBU.encoding === null) {
if (this._sock.rQwait("rect header", 12)) { return false; }
/* New FramebufferUpdate */
const hdr = this._sock.rQshiftBytes(12);
this._FBU.x = (hdr[0] << 8) + hdr[1];
this._FBU.y = (hdr[2] << 8) + hdr[3];
this._FBU.width = (hdr[4] << 8) + hdr[5];
this._FBU.height = (hdr[6] << 8) + hdr[7];
this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) +
(hdr[10] << 8) + hdr[11], 10);
}
if (!this._handleRect()) {
return false;
}
this._FBU.rects--;
this._FBU.encoding = null;
}
this._display.flip();
return true; // We finished this FBU
}
_handleRect() {
switch (this._FBU.encoding) {
case encodings.pseudoEncodingLastRect:
this._FBU.rects = 1; // Will be decreased when we return
return true;
case encodings.pseudoEncodingVMwareCursor:
return this._handleVMwareCursor();
case encodings.pseudoEncodingCursor:
return this._handleCursor();
case encodings.pseudoEncodingQEMUExtendedKeyEvent:
// Old Safari doesn't support creating keyboard events
try {
const keyboardEvent = document.createEvent("keyboardEvent");
if (keyboardEvent.code !== undefined) {
this._qemuExtKeyEventSupported = true;
}
} catch (err) {
// Do nothing
}
return true;
case encodings.pseudoEncodingDesktopName:
return this._handleDesktopName();
case encodings.pseudoEncodingDesktopSize:
this._resize(this._FBU.width, this._FBU.height);
return true;
case encodings.pseudoEncodingExtendedDesktopSize:
return this._handleExtendedDesktopSize();
default:
return this._handleDataRect();
}
}
_handleVMwareCursor() {
const hotx = this._FBU.x; // hotspot-x
const hoty = this._FBU.y; // hotspot-y
const w = this._FBU.width;
const h = this._FBU.height;
if (this._sock.rQwait("VMware cursor encoding", 1)) {
return false;
}
const cursorType = this._sock.rQshift8();
this._sock.rQshift8(); //Padding
let rgba;
const bytesPerPixel = 4;
//Classic cursor
if (cursorType == 0) {
//Used to filter away unimportant bits.
//OR is used for correct conversion in js.
const PIXEL_MASK = 0xffffff00 | 0;
rgba = new Array(w * h * bytesPerPixel);
if (this._sock.rQwait("VMware cursor classic encoding",
(w * h * bytesPerPixel) * 2, 2)) {
return false;
}
let andMask = new Array(w * h);
for (let pixel = 0; pixel < (w * h); pixel++) {
andMask[pixel] = this._sock.rQshift32();
}
let xorMask = new Array(w * h);
for (let pixel = 0; pixel < (w * h); pixel++) {
xorMask[pixel] = this._sock.rQshift32();
}
for (let pixel = 0; pixel < (w * h); pixel++) {
if (andMask[pixel] == 0) {
//Fully opaque pixel
let bgr = xorMask[pixel];
let r = bgr >> 8 & 0xff;
let g = bgr >> 16 & 0xff;
let b = bgr >> 24 & 0xff;
rgba[(pixel * bytesPerPixel) ] = r; //r
rgba[(pixel * bytesPerPixel) + 1 ] = g; //g
rgba[(pixel * bytesPerPixel) + 2 ] = b; //b
rgba[(pixel * bytesPerPixel) + 3 ] = 0xff; //a
} else if ((andMask[pixel] & PIXEL_MASK) ==
PIXEL_MASK) {
//Only screen value matters, no mouse colouring
if (xorMask[pixel] == 0) {
//Transparent pixel
rgba[(pixel * bytesPerPixel) ] = 0x00;
rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
rgba[(pixel * bytesPerPixel) + 3 ] = 0x00;
} else if ((xorMask[pixel] & PIXEL_MASK) ==
PIXEL_MASK) {
//Inverted pixel, not supported in browsers.
//Fully opaque instead.
rgba[(pixel * bytesPerPixel) ] = 0x00;
rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
} else {
//Unhandled xorMask
rgba[(pixel * bytesPerPixel) ] = 0x00;
rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
}
} else {
//Unhandled andMask
rgba[(pixel * bytesPerPixel) ] = 0x00;
rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
}
}
//Alpha cursor.
} else if (cursorType == 1) {
if (this._sock.rQwait("VMware cursor alpha encoding",
(w * h * 4), 2)) {
return false;
}
rgba = new Array(w * h * bytesPerPixel);
for (let pixel = 0; pixel < (w * h); pixel++) {
let data = this._sock.rQshift32();
rgba[(pixel * 4) ] = data >> 24 & 0xff; //r
rgba[(pixel * 4) + 1 ] = data >> 16 & 0xff; //g
rgba[(pixel * 4) + 2 ] = data >> 8 & 0xff; //b
rgba[(pixel * 4) + 3 ] = data & 0xff; //a
}
} else {
Log.Warn("The given cursor type is not supported: "
+ cursorType + " given.");
return false;
}
this._updateCursor(rgba, hotx, hoty, w, h);
return true;
}
_handleCursor() {
const hotx = this._FBU.x; // hotspot-x
const hoty = this._FBU.y; // hotspot-y
const w = this._FBU.width;
const h = this._FBU.height;
const pixelslength = w * h * 4;
const masklength = Math.ceil(w / 8) * h;
let bytes = pixelslength + masklength;
if (this._sock.rQwait("cursor encoding", bytes)) {
return false;
}
// Decode from BGRX pixels + bit mask to RGBA
const pixels = this._sock.rQshiftBytes(pixelslength);
const mask = this._sock.rQshiftBytes(masklength);
let rgba = new Uint8Array(w * h * 4);
let pixIdx = 0;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let maskIdx = y * Math.ceil(w / 8) + Math.floor(x / 8);
let alpha = (mask[maskIdx] << (x % 8)) & 0x80 ? 255 : 0;
rgba[pixIdx ] = pixels[pixIdx + 2];
rgba[pixIdx + 1] = pixels[pixIdx + 1];
rgba[pixIdx + 2] = pixels[pixIdx];
rgba[pixIdx + 3] = alpha;
pixIdx += 4;
}
}
this._updateCursor(rgba, hotx, hoty, w, h);
return true;
}
_handleDesktopName() {
if (this._sock.rQwait("DesktopName", 4)) {
return false;
}
let length = this._sock.rQshift32();
if (this._sock.rQwait("DesktopName", length, 4)) {
return false;
}
let name = this._sock.rQshiftStr(length);
name = decodeUTF8(name, true);
this._setDesktopName(name);
return true;
}
_handleExtendedDesktopSize() {
if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
return false;
}
const numberOfScreens = this._sock.rQpeek8();
let bytes = 4 + (numberOfScreens * 16);
if (this._sock.rQwait("ExtendedDesktopSize", bytes)) {
return false;
}
const firstUpdate = !this._supportsSetDesktopSize;
this._supportsSetDesktopSize = true;
// Normally we only apply the current resize mode after a
// window resize event. However there is no such trigger on the
// initial connect. And we don't know if the server supports
// resizing until we've gotten here.
if (firstUpdate) {
this._requestRemoteResize();
}
this._sock.rQskipBytes(1); // number-of-screens
this._sock.rQskipBytes(3); // padding
for (let i = 0; i < numberOfScreens; i += 1) {
// Save the id and flags of the first screen
if (i === 0) {
this._screenID = this._sock.rQshiftBytes(4); // id
this._sock.rQskipBytes(2); // x-position
this._sock.rQskipBytes(2); // y-position
this._sock.rQskipBytes(2); // width
this._sock.rQskipBytes(2); // height
this._screenFlags = this._sock.rQshiftBytes(4); // flags
} else {
this._sock.rQskipBytes(16);
}
}
/*
* The x-position indicates the reason for the change:
*
* 0 - server resized on its own
* 1 - this client requested the resize
* 2 - another client requested the resize
*/
// We need to handle errors when we requested the resize.
if (this._FBU.x === 1 && this._FBU.y !== 0) {
let msg = "";
// The y-position indicates the status code from the server
switch (this._FBU.y) {
case 1:
msg = "Resize is administratively prohibited";
break;
case 2:
msg = "Out of resources";
break;
case 3:
msg = "Invalid screen layout";
break;
default:
msg = "Unknown reason";
break;
}
Log.Warn("Server did not accept the resize request: "
+ msg);
} else {
this._resize(this._FBU.width, this._FBU.height);
}
return true;
}
_handleDataRect() {
let decoder = this._decoders[this._FBU.encoding];
if (!decoder) {
this._fail("Unsupported encoding (encoding: " +
this._FBU.encoding + ")");
return false;
}
try {
return decoder.decodeRect(this._FBU.x, this._FBU.y,
this._FBU.width, this._FBU.height,
this._sock, this._display,
this._fbDepth);
} catch (err) {
this._fail("Error decoding rect: " + err);
return false;
}
}
_updateContinuousUpdates() {
if (!this._enabledContinuousUpdates) { return; }
RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
this._fbWidth, this._fbHeight);
}
_resize(width, height) {
this._fbWidth = width;
this._fbHeight = height;
this._display.resize(this._fbWidth, this._fbHeight);
// Adjust the visible viewport based on the new dimensions
this._updateClip();
this._updateScale();
this._updateContinuousUpdates();
}
_xvpOp(ver, op) {
if (this._rfbXvpVer < ver) { return; }
Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
RFB.messages.xvpOp(this._sock, ver, op);
}
_updateCursor(rgba, hotx, hoty, w, h) {
this._cursorImage = {
rgbaPixels: rgba,
hotx: hotx, hoty: hoty, w: w, h: h,
};
this._refreshCursor();
}
_shouldShowDotCursor() {
// Called when this._cursorImage is updated
if (!this._showDotCursor) {
// User does not want to see the dot, so...
return false;
}
// The dot should not be shown if the cursor is already visible,
// i.e. contains at least one not-fully-transparent pixel.
// So iterate through all alpha bytes in rgba and stop at the
// first non-zero.
for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) {
if (this._cursorImage.rgbaPixels[i]) {
return false;
}
}
// At this point, we know that the cursor is fully transparent, and
// the user wants to see the dot instead of this.
return true;
}
_refreshCursor() {
if (this._rfbConnectionState !== "connecting" &&
this._rfbConnectionState !== "connected") {
return;
}
const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage;
this._cursor.change(image.rgbaPixels,
image.hotx, image.hoty,
image.w, image.h
);
}
static genDES(password, challenge) {
const passwordChars = password.split('').map(c => c.charCodeAt(0));
return (new DES(passwordChars)).encrypt(challenge);
}
}
// Class Methods
RFB.messages = {
keyEvent(sock, keysym, down) {
const buff = sock._sQ;
const offset = sock._sQlen;
buff[offset] = 4; // msg-type
buff[offset + 1] = down;
buff[offset + 2] = 0;
buff[offset + 3] = 0;
buff[offset + 4] = (keysym >> 24);
buff[offset + 5] = (keysym >> 16);
buff[offset + 6] = (keysym >> 8);
buff[offset + 7] = keysym;
sock._sQlen += 8;
sock.flush();
},
QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
function getRFBkeycode(xtScanCode) {
const upperByte = (keycode >> 8);
const lowerByte = (keycode & 0x00ff);
if (upperByte === 0xe0 && lowerByte < 0x7f) {
return lowerByte | 0x80;
}
return xtScanCode;
}
const buff = sock._sQ;
const offset = sock._sQlen;
buff[offset] = 255; // msg-type
buff[offset + 1] = 0; // sub msg-type
buff[offset + 2] = (down >> 8);
buff[offset + 3] = down;
buff[offset + 4] = (keysym >> 24);
buff[offset + 5] = (keysym >> 16);
buff[offset + 6] = (keysym >> 8);
buff[offset + 7] = keysym;
const RFBkeycode = getRFBkeycode(keycode);
buff[offset + 8] = (RFBkeycode >> 24);
buff[offset + 9] = (RFBkeycode >> 16);
buff[offset + 10] = (RFBkeycode >> 8);
buff[offset + 11] = RFBkeycode;
sock._sQlen += 12;
sock.flush();
},
pointerEvent(sock, x, y, mask) {
const buff = sock._sQ;
const offset = sock._sQlen;
buff[offset] = 5; // msg-type
buff[offset + 1] = mask;
buff[offset + 2] = x >> 8;
buff[offset + 3] = x;
buff[offset + 4] = y >> 8;
buff[offset + 5] = y;
sock._sQlen += 6;
sock.flush();
},
// Used to build Notify and Request data.
_buildExtendedClipboardFlags(actions, formats) {
let data = new Uint8Array(4);
let formatFlag = 0x00000000;
let actionFlag = 0x00000000;
for (let i = 0; i < actions.length; i++) {
actionFlag |= actions[i];
}
for (let i = 0; i < formats.length; i++) {
formatFlag |= formats[i];
}
data[0] = actionFlag >> 24; // Actions
data[1] = 0x00; // Reserved
data[2] = 0x00; // Reserved
data[3] = formatFlag; // Formats
return data;
},
extendedClipboardProvide(sock, formats, inData) {
// Deflate incoming data and their sizes
let deflator = new Deflator();
let dataToDeflate = [];
for (let i = 0; i < formats.length; i++) {
// We only support the format Text at this time
if (formats[i] != extendedClipboardFormatText) {
throw new Error("Unsupported extended clipboard format for Provide message.");
}
// Change lone \r or \n into \r\n as defined in rfbproto
inData[i] = inData[i].replace(/\r\n|\r|\n/gm, "\r\n");
// Check if it already has \0
let text = encodeUTF8(inData[i] + "\0");
dataToDeflate.push( (text.length >> 24) & 0xFF,
(text.length >> 16) & 0xFF,
(text.length >> 8) & 0xFF,
(text.length & 0xFF));
for (let j = 0; j < text.length; j++) {
dataToDeflate.push(text.charCodeAt(j));
}
}
let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate));
// Build data to send
let data = new Uint8Array(4 + deflatedData.length);
data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide],
formats));
data.set(deflatedData, 4);
RFB.messages.clientCutText(sock, data, true);
},
extendedClipboardNotify(sock, formats) {
let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify],
formats);
RFB.messages.clientCutText(sock, flags, true);
},
extendedClipboardRequest(sock, formats) {
let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest],
formats);
RFB.messages.clientCutText(sock, flags, true);
},
extendedClipboardCaps(sock, actions, formats) {
let formatKeys = Object.keys(formats);
let data = new Uint8Array(4 + (4 * formatKeys.length));
formatKeys.map(x => parseInt(x));
formatKeys.sort((a, b) => a - b);
data.set(RFB.messages._buildExtendedClipboardFlags(actions, []));
let loopOffset = 4;
for (let i = 0; i < formatKeys.length; i++) {
data[loopOffset] = formats[formatKeys[i]] >> 24;
data[loopOffset + 1] = formats[formatKeys[i]] >> 16;
data[loopOffset + 2] = formats[formatKeys[i]] >> 8;
data[loopOffset + 3] = formats[formatKeys[i]] >> 0;
loopOffset += 4;
data[3] |= (1 << formatKeys[i]); // Update our format flags
}
RFB.messages.clientCutText(sock, data, true);
},
clientCutText(sock, data, extended = false) {
const buff = sock._sQ;
const offset = sock._sQlen;
buff[offset] = 6; // msg-type
buff[offset + 1] = 0; // padding
buff[offset + 2] = 0; // padding
buff[offset + 3] = 0; // padding
let length;
if (extended) {
length = toUnsigned32bit(-data.length);
} else {
length = data.length;
}
buff[offset + 4] = length >> 24;
buff[offset + 5] = length >> 16;
buff[offset + 6] = length >> 8;
buff[offset + 7] = length;
sock._sQlen += 8;
// We have to keep track of from where in the data we begin creating the
// buffer for the flush in the next iteration.
let dataOffset = 0;
let remaining = data.length;
while (remaining > 0) {
let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
for (let i = 0; i < flushSize; i++) {
buff[sock._sQlen + i] = data[dataOffset + i];
}
sock._sQlen += flushSize;
sock.flush();
remaining -= flushSize;
dataOffset += flushSize;
}
},
setDesktopSize(sock, width, height, id, flags) {
const buff = sock._sQ;
const offset = sock._sQlen;
buff[offset] = 251; // msg-type
buff[offset + 1] = 0; // padding
buff[offset + 2] = width >> 8; // width
buff[offset + 3] = width;
buff[offset + 4] = height >> 8; // height
buff[offset + 5] = height;
buff[offset + 6] = 1; // number-of-screens
buff[offset + 7] = 0; // padding
// screen array
buff[offset + 8] = id >> 24; // id
buff[offset + 9] = id >> 16;
buff[offset + 10] = id >> 8;
buff[offset + 11] = id;
buff[offset + 12] = 0; // x-position
buff[offset + 13] = 0;
buff[offset + 14] = 0; // y-position
buff[offset + 15] = 0;
buff[offset + 16] = width >> 8; // width
buff[offset + 17] = width;
buff[offset + 18] = height >> 8; // height
buff[offset + 19] = height;
buff[offset + 20] = flags >> 24; // flags
buff[offset + 21] = flags >> 16;
buff[offset + 22] = flags >> 8;
buff[offset + 23] = flags;
sock._sQlen += 24;
sock.flush();
},
clientFence(sock, flags, payload) {
const buff = sock._sQ;
const offset = sock._sQlen;
buff[offset] = 248; // msg-type
buff[offset + 1] = 0; // padding
buff[offset + 2] = 0; // padding
buff[offset + 3] = 0; // padding
buff[offset + 4] = flags >> 24; // flags
buff[offset + 5] = flags >> 16;
buff[offset + 6] = flags >> 8;
buff[offset + 7] = flags;
const n = payload.length;
buff[offset + 8] = n; // length
for (let i = 0; i < n; i++) {
buff[offset + 9 + i] = payload.charCodeAt(i);
}
sock._sQlen += 9 + n;
sock.flush();
},
enableContinuousUpdates(sock, enable, x, y, width, height) {
const buff = sock._sQ;
const offset = sock._sQlen;
buff[offset] = 150; // msg-type
buff[offset + 1] = enable; // enable-flag
buff[offset + 2] = x >> 8; // x
buff[offset + 3] = x;
buff[offset + 4] = y >> 8; // y
buff[offset + 5] = y;
buff[offset + 6] = width >> 8; // width
buff[offset + 7] = width;
buff[offset + 8] = height >> 8; // height
buff[offset + 9] = height;
sock._sQlen += 10;
sock.flush();
},
pixelFormat(sock, depth, trueColor) {
const buff = sock._sQ;
const offset = sock._sQlen;
let bpp;
if (depth > 16) {
bpp = 32;
} else if (depth > 8) {
bpp = 16;
} else {
bpp = 8;
}
const bits = Math.floor(depth/3);
buff[offset] = 0; // msg-type
buff[offset + 1] = 0; // padding
buff[offset + 2] = 0; // padding
buff[offset + 3] = 0; // padding
buff[offset + 4] = bpp; // bits-per-pixel
buff[offset + 5] = depth; // depth
buff[offset + 6] = 0; // little-endian
buff[offset + 7] = trueColor ? 1 : 0; // true-color
buff[offset + 8] = 0; // red-max
buff[offset + 9] = (1 << bits) - 1; // red-max
buff[offset + 10] = 0; // green-max
buff[offset + 11] = (1 << bits) - 1; // green-max
buff[offset + 12] = 0; // blue-max
buff[offset + 13] = (1 << bits) - 1; // blue-max
buff[offset + 14] = bits * 0; // red-shift
buff[offset + 15] = bits * 1; // green-shift
buff[offset + 16] = bits * 2; // blue-shift
buff[offset + 17] = 0; // padding
buff[offset + 18] = 0; // padding
buff[offset + 19] = 0; // padding
sock._sQlen += 20;
sock.flush();
},
clientEncodings(sock, encodings) {
const buff = sock._sQ;
const offset = sock._sQlen;
buff[offset] = 2; // msg-type
buff[offset + 1] = 0; // padding
buff[offset + 2] = encodings.length >> 8;
buff[offset + 3] = encodings.length;
let j = offset + 4;
for (let i = 0; i < encodings.length; i++) {
const enc = encodings[i];
buff[j] = enc >> 24;
buff[j + 1] = enc >> 16;
buff[j + 2] = enc >> 8;
buff[j + 3] = enc;
j += 4;
}
sock._sQlen += j - offset;
sock.flush();
},
fbUpdateRequest(sock, incremental, x, y, w, h) {
const buff = sock._sQ;
const offset = sock._sQlen;
if (typeof(x) === "undefined") { x = 0; }
if (typeof(y) === "undefined") { y = 0; }
buff[offset] = 3; // msg-type
buff[offset + 1] = incremental ? 1 : 0;
buff[offset + 2] = (x >> 8) & 0xFF;
buff[offset + 3] = x & 0xFF;
buff[offset + 4] = (y >> 8) & 0xFF;
buff[offset + 5] = y & 0xFF;
buff[offset + 6] = (w >> 8) & 0xFF;
buff[offset + 7] = w & 0xFF;
buff[offset + 8] = (h >> 8) & 0xFF;
buff[offset + 9] = h & 0xFF;
sock._sQlen += 10;
sock.flush();
},
xvpOp(sock, ver, op) {
const buff = sock._sQ;
const offset = sock._sQlen;
buff[offset] = 250; // msg-type
buff[offset + 1] = 0; // padding
buff[offset + 2] = ver;
buff[offset + 3] = op;
sock._sQlen += 4;
sock.flush();
}
};
RFB.cursors = {
none: {
rgbaPixels: new Uint8Array(),
w: 0, h: 0,
hotx: 0, hoty: 0,
},
dot: {
/* eslint-disable indent */
rgbaPixels: new Uint8Array([
255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,
255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,
]),
/* eslint-enable indent */
w: 3, h: 3,
hotx: 1, hoty: 1,
}
};