mirror of
https://github.com/kasmtech/KasmVNC.git
synced 2024-11-30 20:13:20 +01:00
766fc43855
Supports both classic cursor type and alpha cursor type. In classic mode the server can send 'inverted' pixels for the cursor, our code does not support this but handles these pixels as opaque black. Co-authored-by: Samuel Mannehed <samuel@cendio.se>
2423 lines
81 KiB
JavaScript
2423 lines
81 KiB
JavaScript
/*
|
|
* noVNC: HTML5 VNC client
|
|
* Copyright (C) 2018 The noVNC Authors
|
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
|
*
|
|
* See README.md for usage and integration instructions.
|
|
*
|
|
*/
|
|
|
|
import * as Log from './util/logging.js';
|
|
import { decodeUTF8 } from './util/strings.js';
|
|
import { supportsCursorURIs, isTouchDevice } from './util/browser.js';
|
|
import { dragThreshold } from './util/browser.js';
|
|
import EventTargetMixin from './util/eventtarget.js';
|
|
import Display from "./display.js";
|
|
import Keyboard from "./input/keyboard.js";
|
|
import Mouse from "./input/mouse.js";
|
|
import Cursor from "./util/cursor.js";
|
|
import Websock from "./websock.js";
|
|
import DES from "./des.js";
|
|
import KeyTable from "./input/keysym.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)';
|
|
|
|
var _videoQuality = 2;
|
|
var _enableWebP = false;
|
|
|
|
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._rfb_credentials = options.credentials || {};
|
|
this._shared = 'shared' in options ? !!options.shared : true;
|
|
this._repeaterID = options.repeaterID || '';
|
|
this._wsProtocols = options.wsProtocols || [];
|
|
|
|
// Internal state
|
|
this._rfb_connection_state = '';
|
|
this._rfb_init_state = '';
|
|
this._rfb_auth_scheme = -1;
|
|
this._rfb_clean_disconnect = true;
|
|
|
|
// Server capabilities
|
|
this._rfb_version = 0;
|
|
this._rfb_max_version = 3.8;
|
|
this._rfb_tightvnc = false;
|
|
this._rfb_xvp_ver = 0;
|
|
|
|
this._fb_width = 0;
|
|
this._fb_height = 0;
|
|
|
|
this._fb_name = "";
|
|
|
|
this._capabilities = { power: false };
|
|
|
|
this._supportsFence = false;
|
|
|
|
this._supportsContinuousUpdates = false;
|
|
this._enabledContinuousUpdates = false;
|
|
|
|
this._supportsSetDesktopSize = false;
|
|
this._screen_id = 0;
|
|
this._screen_flags = 0;
|
|
|
|
this._qemuExtKeyEventSupported = false;
|
|
|
|
// kasm defaults
|
|
this._jpegVideoQuality = 5;
|
|
this._webpVideoQuality = 5;
|
|
this._treatLossless = 7;
|
|
this._preferBandwidth = true;
|
|
this._dynamicQualityMin = 3;
|
|
this._dynamicQualityMax = 9;
|
|
this._videoArea = 65;
|
|
this._videoTime = 5;
|
|
this._videoOutTime = 3;
|
|
this._videoScaling = 2;
|
|
this._frameRate = 30;
|
|
this._maxVideoResolutionX = 960;
|
|
this._maxVideoResolutionY = 540;
|
|
|
|
// 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._mouse = null; // Mouse input handler object
|
|
|
|
// Timers
|
|
this._disconnTimer = null; // disconnection timer
|
|
this._resizeTimeout = null; // resize rate limiting
|
|
|
|
// Decoder states
|
|
this._decoders = {};
|
|
|
|
this._FBU = {
|
|
rects: 0,
|
|
x: 0,
|
|
y: 0,
|
|
width: 0,
|
|
height: 0,
|
|
encoding: null,
|
|
};
|
|
|
|
// Mouse state
|
|
this._mouse_buttonMask = 0;
|
|
this._mouse_arr = [];
|
|
this._viewportDragging = false;
|
|
this._viewportDragPos = {};
|
|
this._viewportHasMoved = false;
|
|
|
|
// Bound event handlers
|
|
this._eventHandlers = {
|
|
focusCanvas: this._focusCanvas.bind(this),
|
|
windowResize: this._windowResize.bind(this),
|
|
firstClick: this._firstClick.bind(this),
|
|
firstTouch: this._firstTouch.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._mouse = new Mouse(this._canvas);
|
|
this._mouse.onmousebutton = this._handleMouseButton.bind(this);
|
|
this._mouse.onmousemove = this._handleMouseMove.bind(this);
|
|
|
|
this._sock = new Websock();
|
|
this._sock.on('message', () => {
|
|
this._handle_message();
|
|
});
|
|
this._sock.on('open', () => {
|
|
if ((this._rfb_connection_state === 'connecting') &&
|
|
(this._rfb_init_state === '')) {
|
|
this._rfb_init_state = 'ProtocolVersion';
|
|
Log.Debug("Starting VNC handshake");
|
|
} else {
|
|
this._fail("Unexpected server connection while " +
|
|
this._rfb_connection_state);
|
|
}
|
|
});
|
|
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._rfb_connection_state) {
|
|
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.sentEventsCounter = 0; //tracks the number of events sent to the server
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// ===== PROPERTIES =====
|
|
|
|
get videoQuality() { return this._videoQuality; }
|
|
set videoQuality(quality) { this._videoQuality = quality; }
|
|
|
|
get enableWebP() { return this._enableWebP; }
|
|
set enableWebP(enabled) { this._enableWebP = enabled; }
|
|
|
|
get jpegVideoQuality() { return this._jpegVideoQuality; }
|
|
set jpegVideoQuality(val) { this._jpegVideoQuality = val; }
|
|
|
|
get webpVideoQuality() { return this._webpVideoQuality; }
|
|
set webpVideoQuality(val) { this._webpVideoQuality = val; }
|
|
|
|
get treatLossless() { return this._treatLossless; }
|
|
set treatLossless(val) { this._treatLossless = val; }
|
|
|
|
get preferBandwidth() { return this._preferBandwidth; }
|
|
set preferBandwidth(val) { this._preferBandwidth = val; }
|
|
|
|
get dynamicQualityMin() { return this._dynamicQualityMin; }
|
|
set dynamicQualityMin(val) { this._dynamicQualityMin = val; }
|
|
|
|
get dynamicQualityMax() { return this._dynamicQualityMax; }
|
|
set dynamicQualityMax(val) { this._dynamicQualityMax = val; }
|
|
|
|
get videoArea() { return this._videoArea; }
|
|
set videoArea(val) { this._videoArea = val; }
|
|
|
|
get videoTime() { return this._videoTime; }
|
|
set videoTime(val) { this._videoTime = val; }
|
|
|
|
get videoOutTime() { return this._videoOutTime; }
|
|
set videoOutTime(val) { this._videoOutTime = val; }
|
|
|
|
get videoScaling() { return this._videoScaling; }
|
|
set videoScaling(val) { this._videoScaling = val; }
|
|
|
|
get frameRate() { return this._frameRate; }
|
|
set frameRate(val) { this._frameRate = val; }
|
|
|
|
get maxVideoResolutionX() { return this._maxVideoResolutionX; }
|
|
set maxVideoResolutionX(val) { this._maxVideoResolutionX = val; }
|
|
|
|
get maxVideoResolutionY() { return this._maxVideoResolutionY; }
|
|
set maxVideoResolutionY(val) { this._maxVideoResolutionY = val; }
|
|
|
|
get viewOnly() { return this._viewOnly; }
|
|
set viewOnly(viewOnly) {
|
|
this._viewOnly = viewOnly;
|
|
|
|
if (this._rfb_connection_state === "connecting" ||
|
|
this._rfb_connection_state === "connected") {
|
|
if (viewOnly) {
|
|
this._keyboard.ungrab();
|
|
this._mouse.ungrab();
|
|
} else {
|
|
this._keyboard.grab();
|
|
this._mouse.grab();
|
|
}
|
|
}
|
|
}
|
|
|
|
get capabilities() { return this._capabilities; }
|
|
|
|
get touchButton() { return this._mouse.touchButton; }
|
|
set touchButton(button) { this._mouse.touchButton = button; }
|
|
|
|
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();
|
|
this.scaleViewport = true;
|
|
}
|
|
}
|
|
|
|
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; }
|
|
|
|
// ===== PUBLIC METHODS =====
|
|
|
|
disconnect() {
|
|
this._updateConnectionState('disconnecting');
|
|
this._sock.off('error');
|
|
this._sock.off('message');
|
|
this._sock.off('open');
|
|
}
|
|
|
|
sendCredentials(creds) {
|
|
this._rfb_credentials = creds;
|
|
setTimeout(this._init_msg.bind(this), 0);
|
|
}
|
|
|
|
sendCtrlAltDel() {
|
|
if (this._rfb_connection_state !== '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);
|
|
}
|
|
|
|
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._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
|
|
|
|
this.sentEventsCounter+=1;
|
|
|
|
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._rfb_connection_state !== 'connected' || this._viewOnly) { return; }
|
|
this.sentEventsCounter+=1;
|
|
RFB.messages.clientCutText(this._sock, text);
|
|
}
|
|
|
|
requestBottleneckStats() {
|
|
RFB.messages.requestStats(this._sock);
|
|
}
|
|
|
|
// ===== 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);
|
|
this.sentEventsCounter+=1;
|
|
} 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._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);
|
|
this._canvas.addEventListener("mousedown", this._eventHandlers.firstClick);
|
|
this._canvas.addEventListener("touchstart", this._eventHandlers.firstTouch);
|
|
|
|
Log.Debug("<< RFB.connect");
|
|
}
|
|
|
|
_disconnect() {
|
|
Log.Debug(">> RFB.disconnect");
|
|
this._cursor.detach();
|
|
this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
|
|
this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
|
|
window.removeEventListener('resize', this._eventHandlers.windowResize);
|
|
this._keyboard.ungrab();
|
|
this._mouse.ungrab();
|
|
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);
|
|
Log.Debug("<< RFB.disconnect");
|
|
}
|
|
|
|
_focusCanvas(event) {
|
|
// Respect earlier handlers' request to not do side-effects
|
|
if (event.defaultPrevented) {
|
|
return;
|
|
}
|
|
|
|
if (!this.focusOnClick) {
|
|
return;
|
|
}
|
|
|
|
this.focus();
|
|
}
|
|
|
|
_setDesktopName(name) {
|
|
this._fb_name = name;
|
|
this.dispatchEvent(new CustomEvent(
|
|
"desktopname",
|
|
{ detail: { name: this._fb_name } }));
|
|
}
|
|
|
|
_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);
|
|
}
|
|
}
|
|
|
|
_firstClick(event) {
|
|
parent.postMessage({ action: 'firstClick', value: null}, '*' );
|
|
this._canvas.removeEventListener("mousedown", this._eventHandlers.firstClick);
|
|
}
|
|
|
|
_firstTouch(event) {
|
|
parent.postMessage({ action: 'firstTouch', value: null}, '*' );
|
|
this._canvas.removeEventListener("touchstart", this._eventHandlers.firstTouch);
|
|
}
|
|
|
|
// Update state of clipping in Display object, and make sure the
|
|
// configured viewport matches the current screen size
|
|
_updateClip() {
|
|
const cur_clip = this._display.clipViewport;
|
|
let new_clip = this._clipViewport;
|
|
|
|
if (this._scaleViewport) {
|
|
// Disable viewport clipping if we are scaling
|
|
new_clip = false;
|
|
}
|
|
|
|
if (cur_clip !== new_clip) {
|
|
this._display.clipViewport = new_clip;
|
|
}
|
|
|
|
if (new_clip) {
|
|
// 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(false);
|
|
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._screen_id, this._screen_flags);
|
|
|
|
this.sentEventsCounter+=1;
|
|
|
|
Log.Debug('Requested new desktop size: ' +
|
|
size.w + 'x' + size.h);
|
|
}
|
|
|
|
// Gets the the size of the available screen
|
|
_screenSize (limited) {
|
|
if (limited === undefined) {
|
|
limited = true;
|
|
}
|
|
var x = this._screen.offsetWidth;
|
|
var y = this._screen.offsetHeight;
|
|
try {
|
|
if (x > 1280 && limited && this.videoQuality == 1) {
|
|
var ratio = y / x;
|
|
console.log(ratio);
|
|
x = 1280;
|
|
y = x * ratio;
|
|
}
|
|
else if (limited && this.videoQuality == 0){
|
|
x = 1280;
|
|
y = 720;
|
|
}
|
|
} catch (err) {
|
|
console.log(err);
|
|
}
|
|
|
|
return { w: x,
|
|
h: y };
|
|
}
|
|
|
|
_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._rfb_connection_state;
|
|
|
|
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._rfb_connection_state = 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._rfb_clean_disconnect } }));
|
|
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._rfb_connection_state) {
|
|
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._rfb_clean_disconnect = 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 } }));
|
|
}
|
|
|
|
_handle_message() {
|
|
if (this._sock.rQlen === 0) {
|
|
Log.Warn("handle_message called on an empty receive queue");
|
|
return;
|
|
}
|
|
|
|
switch (this._rfb_connection_state) {
|
|
case 'disconnected':
|
|
Log.Error("Got data while disconnected");
|
|
break;
|
|
case 'connected':
|
|
while (true) {
|
|
if (this._flushing) {
|
|
break;
|
|
}
|
|
if (!this._normal_msg()) {
|
|
break;
|
|
}
|
|
if (this._sock.rQlen === 0) {
|
|
break;
|
|
}
|
|
}
|
|
break;
|
|
default:
|
|
this._init_msg();
|
|
break;
|
|
}
|
|
}
|
|
|
|
_handleKeyEvent(keysym, code, down) {
|
|
this.sendKey(keysym, code, down);
|
|
}
|
|
|
|
_handleMouseButton(x, y, down, bmask) {
|
|
if (down) {
|
|
this._mouse_buttonMask |= bmask;
|
|
} else {
|
|
this._mouse_buttonMask &= ~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.sentEventsCounter+=1;
|
|
RFB.messages.pointerEvent(this._sock,
|
|
this._display.absX(x),
|
|
this._display.absY(y),
|
|
bmask);
|
|
}
|
|
}
|
|
|
|
if (this._viewOnly) { return; } // View only, skip mouse events
|
|
|
|
if (this._rfb_connection_state !== 'connected') { return; }
|
|
this.sentEventsCounter+=1;
|
|
RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
|
|
}
|
|
|
|
_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;
|
|
}
|
|
|
|
if (this._viewOnly) { return; } // View only, skip mouse events
|
|
|
|
if (this._rfb_connection_state !== 'connected') { return; }
|
|
RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask);
|
|
}
|
|
|
|
// Message Handlers
|
|
|
|
_negotiate_protocol_version() {
|
|
if (this._sock.rQwait("version", 12)) {
|
|
return false;
|
|
}
|
|
|
|
const sversion = this._sock.rQshiftStr(12).substr(4, 7);
|
|
Log.Info("Server ProtocolVersion: " + sversion);
|
|
let is_repeater = 0;
|
|
switch (sversion) {
|
|
case "000.000": // UltraVNC repeater
|
|
is_repeater = 1;
|
|
break;
|
|
case "003.003":
|
|
case "003.006": // UltraVNC
|
|
case "003.889": // Apple Remote Desktop
|
|
this._rfb_version = 3.3;
|
|
break;
|
|
case "003.007":
|
|
this._rfb_version = 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._rfb_version = 3.8;
|
|
break;
|
|
default:
|
|
return this._fail("Invalid server version " + sversion);
|
|
}
|
|
|
|
if (is_repeater) {
|
|
let repeaterID = "ID:" + this._repeaterID;
|
|
while (repeaterID.length < 250) {
|
|
repeaterID += "\0";
|
|
}
|
|
this._sock.send_string(repeaterID);
|
|
return true;
|
|
}
|
|
|
|
if (this._rfb_version > this._rfb_max_version) {
|
|
this._rfb_version = this._rfb_max_version;
|
|
}
|
|
|
|
const cversion = "00" + parseInt(this._rfb_version, 10) +
|
|
".00" + ((this._rfb_version * 10) % 10);
|
|
this._sock.send_string("RFB " + cversion + "\n");
|
|
Log.Debug('Sent ProtocolVersion: ' + cversion);
|
|
|
|
this._rfb_init_state = 'Security';
|
|
}
|
|
|
|
_negotiate_security() {
|
|
// 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._rfb_version >= 3.7) {
|
|
// Server sends supported list, client decides
|
|
const num_types = this._sock.rQshift8();
|
|
if (this._sock.rQwait("security type", num_types, 1)) { return false; }
|
|
|
|
if (num_types === 0) {
|
|
this._rfb_init_state = "SecurityReason";
|
|
this._security_context = "no security types";
|
|
this._security_status = 1;
|
|
return this._init_msg();
|
|
}
|
|
|
|
const types = this._sock.rQshiftBytes(num_types);
|
|
Log.Debug("Server security types: " + types);
|
|
|
|
// Look for each auth in preferred order
|
|
if (includes(1, types)) {
|
|
this._rfb_auth_scheme = 1; // None
|
|
} else if (includes(22, types)) {
|
|
this._rfb_auth_scheme = 22; // XVP
|
|
} else if (includes(16, types)) {
|
|
this._rfb_auth_scheme = 16; // Tight
|
|
} else if (includes(2, types)) {
|
|
this._rfb_auth_scheme = 2; // VNC Auth
|
|
} else {
|
|
return this._fail("Unsupported security types (types: " + types + ")");
|
|
}
|
|
|
|
this._sock.send([this._rfb_auth_scheme]);
|
|
} else {
|
|
// Server decides
|
|
if (this._sock.rQwait("security scheme", 4)) { return false; }
|
|
this._rfb_auth_scheme = this._sock.rQshift32();
|
|
|
|
if (this._rfb_auth_scheme == 0) {
|
|
this._rfb_init_state = "SecurityReason";
|
|
this._security_context = "authentication scheme";
|
|
this._security_status = 1;
|
|
return this._init_msg();
|
|
}
|
|
}
|
|
|
|
this._rfb_init_state = 'Authentication';
|
|
Log.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme);
|
|
|
|
return this._init_msg(); // jump to authentication
|
|
}
|
|
|
|
_handle_security_reason() {
|
|
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._security_status,
|
|
reason: reason } }));
|
|
|
|
return this._fail("Security negotiation failed on " +
|
|
this._security_context +
|
|
" (reason: " + reason + ")");
|
|
} else {
|
|
this.dispatchEvent(new CustomEvent(
|
|
"securityfailure",
|
|
{ detail: { status: this._security_status } }));
|
|
|
|
return this._fail("Security negotiation failed on " +
|
|
this._security_context);
|
|
}
|
|
}
|
|
|
|
// authentication
|
|
_negotiate_xvp_auth() {
|
|
if (!this._rfb_credentials.username ||
|
|
!this._rfb_credentials.password ||
|
|
!this._rfb_credentials.target) {
|
|
this.dispatchEvent(new CustomEvent(
|
|
"credentialsrequired",
|
|
{ detail: { types: ["username", "password", "target"] } }));
|
|
return false;
|
|
}
|
|
|
|
const xvp_auth_str = String.fromCharCode(this._rfb_credentials.username.length) +
|
|
String.fromCharCode(this._rfb_credentials.target.length) +
|
|
this._rfb_credentials.username +
|
|
this._rfb_credentials.target;
|
|
this._sock.send_string(xvp_auth_str);
|
|
this._rfb_auth_scheme = 2;
|
|
return this._negotiate_authentication();
|
|
}
|
|
|
|
_negotiate_std_vnc_auth() {
|
|
if (this._sock.rQwait("auth challenge", 16)) { return false; }
|
|
|
|
// KasmVNC uses basic Auth, clear the VNC password, which is not used
|
|
this._rfb_credentials.password = "";
|
|
|
|
// TODO(directxman12): make genDES not require an Array
|
|
const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
|
|
const response = RFB.genDES(this._rfb_credentials.password, challenge);
|
|
this._sock.send(response);
|
|
this._rfb_init_state = "SecurityResult";
|
|
return true;
|
|
}
|
|
|
|
_negotiate_tight_tunnels(numTunnels) {
|
|
const clientSupportedTunnelTypes = {
|
|
0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
|
|
};
|
|
const serverSupportedTunnelTypes = {};
|
|
// receive tunnel capabilities
|
|
for (let i = 0; i < numTunnels; i++) {
|
|
const cap_code = this._sock.rQshift32();
|
|
const cap_vendor = this._sock.rQshiftStr(4);
|
|
const cap_signature = this._sock.rQshiftStr(8);
|
|
serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature };
|
|
}
|
|
|
|
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");
|
|
}
|
|
}
|
|
|
|
_negotiate_tight_auth() {
|
|
if (!this._rfb_tightvnc) { // 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._rfb_tightvnc = true;
|
|
|
|
if (numTunnels > 0) {
|
|
this._negotiate_tight_tunnels(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._rfb_init_state = 'SecurityResult';
|
|
return true;
|
|
}
|
|
|
|
if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
|
|
|
|
const clientSupportedTypes = {
|
|
'STDVNOAUTH__': 1,
|
|
'STDVVNCAUTH_': 2
|
|
};
|
|
|
|
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._rfb_init_state = 'SecurityResult';
|
|
return true;
|
|
case 'STDVVNCAUTH_': // VNC auth
|
|
this._rfb_auth_scheme = 2;
|
|
return this._init_msg();
|
|
default:
|
|
return this._fail("Unsupported tiny auth scheme " +
|
|
"(scheme: " + authType + ")");
|
|
}
|
|
}
|
|
}
|
|
|
|
return this._fail("No supported sub-auth types!");
|
|
}
|
|
|
|
_negotiate_authentication() {
|
|
switch (this._rfb_auth_scheme) {
|
|
case 1: // no auth
|
|
if (this._rfb_version >= 3.8) {
|
|
this._rfb_init_state = 'SecurityResult';
|
|
return true;
|
|
}
|
|
this._rfb_init_state = 'ClientInitialisation';
|
|
return this._init_msg();
|
|
|
|
case 22: // XVP auth
|
|
return this._negotiate_xvp_auth();
|
|
|
|
case 2: // VNC authentication
|
|
return this._negotiate_std_vnc_auth();
|
|
|
|
case 16: // TightVNC Security Type
|
|
return this._negotiate_tight_auth();
|
|
|
|
default:
|
|
return this._fail("Unsupported auth scheme (scheme: " +
|
|
this._rfb_auth_scheme + ")");
|
|
}
|
|
}
|
|
|
|
_handle_security_result() {
|
|
if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
|
|
|
|
const status = this._sock.rQshift32();
|
|
|
|
if (status === 0) { // OK
|
|
this._rfb_init_state = 'ClientInitialisation';
|
|
Log.Debug('Authentication OK');
|
|
return this._init_msg();
|
|
} else {
|
|
if (this._rfb_version >= 3.8) {
|
|
this._rfb_init_state = "SecurityReason";
|
|
this._security_context = "security result";
|
|
this._security_status = status;
|
|
return this._init_msg();
|
|
} else {
|
|
this.dispatchEvent(new CustomEvent(
|
|
"securityfailure",
|
|
{ detail: { status: status } }));
|
|
|
|
return this._fail("Security handshake failed");
|
|
}
|
|
}
|
|
}
|
|
|
|
_negotiate_server_init() {
|
|
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 big_endian = this._sock.rQshift8();
|
|
const true_color = this._sock.rQshift8();
|
|
|
|
const red_max = this._sock.rQshift16();
|
|
const green_max = this._sock.rQshift16();
|
|
const blue_max = this._sock.rQshift16();
|
|
const red_shift = this._sock.rQshift8();
|
|
const green_shift = this._sock.rQshift8();
|
|
const blue_shift = 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 name_length = this._sock.rQshift32();
|
|
if (this._sock.rQwait('server init name', name_length, 24)) { return false; }
|
|
let name = decodeUTF8(this._sock.rQshiftStr(name_length));
|
|
|
|
if (this._rfb_tightvnc) {
|
|
if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { 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 + name_length)) { 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 +
|
|
", big_endian: " + big_endian +
|
|
", true_color: " + true_color +
|
|
", red_max: " + red_max +
|
|
", green_max: " + green_max +
|
|
", blue_max: " + blue_max +
|
|
", red_shift: " + red_shift +
|
|
", green_shift: " + green_shift +
|
|
", blue_shift: " + blue_shift);
|
|
|
|
// 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(); }
|
|
if (!this._viewOnly) { this._mouse.grab(); }
|
|
|
|
this._fb_depth = 24;
|
|
|
|
if (this._fb_name === "Intel(r) AMT KVM") {
|
|
Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
|
|
this._fb_depth = 8;
|
|
}
|
|
|
|
RFB.messages.pixelFormat(this._sock, this._fb_depth, true);
|
|
this._sendEncodings();
|
|
RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height);
|
|
|
|
this._updateConnectionState('connected');
|
|
return true;
|
|
}
|
|
|
|
_hasWebp() {
|
|
/*
|
|
return new Promise(res => {
|
|
const webP = new Image();
|
|
webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA';
|
|
webP.onload = webP.onerror = function () {
|
|
res(webP.height === 2);
|
|
};
|
|
})
|
|
*/
|
|
if (!this.enableWebP)
|
|
return false;
|
|
// It's not possible to check for webp synchronously, and hacking promises
|
|
// into everything would be too time-consuming. So test for FF and Chrome.
|
|
var uagent = navigator.userAgent.toLowerCase();
|
|
var match = uagent.match(/firefox\/([0-9]+)\./);
|
|
if (match && parseInt(match[1]) >= 65)
|
|
return true;
|
|
match = uagent.match(/chrome\/([0-9]+)\./);
|
|
if (match && parseInt(match[1]) >= 23)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
_sendEncodings() {
|
|
const encs = [];
|
|
var hasWebp;
|
|
|
|
// In preference order
|
|
encs.push(encodings.encodingCopyRect);
|
|
// Only supported with full depth support
|
|
if (this._fb_depth == 24) {
|
|
encs.push(encodings.encodingTight);
|
|
encs.push(encodings.encodingTightPNG);
|
|
encs.push(encodings.encodingHextile);
|
|
encs.push(encodings.encodingRRE);
|
|
}
|
|
encs.push(encodings.encodingRaw);
|
|
|
|
// Psuedo-encoding settings
|
|
var quality = 6;
|
|
var compression = 2;
|
|
var screensize = this._screenSize(false);
|
|
if (this.videoQuality == 1) {
|
|
if (screensize.w > 1280) {
|
|
quality = 8; //higher quality needed because scaling enlarges artifacts
|
|
} else {
|
|
quality = 3; //twice the compression ratio as default, but not horrible quality
|
|
}
|
|
compression = 6;
|
|
} else if (this.videoQuality == 3) {
|
|
quality = 8
|
|
}
|
|
encs.push(encodings.pseudoEncodingQualityLevel0 + quality);
|
|
encs.push(encodings.pseudoEncodingCompressLevel0 + compression);
|
|
|
|
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);
|
|
if (this._hasWebp())
|
|
encs.push(encodings.pseudoEncodingWEBP);
|
|
|
|
// kasm settings; the server may be configured to ignore these
|
|
encs.push(encodings.pseudoEncodingJpegVideoQualityLevel0 + this.jpegVideoQuality);
|
|
encs.push(encodings.pseudoEncodingWebpVideoQualityLevel0 + this.webpVideoQuality);
|
|
encs.push(encodings.pseudoEncodingTreatLosslessLevel0 + this.treatLossless);
|
|
encs.push(encodings.pseudoEncodingDynamicQualityMinLevel0 + this.dynamicQualityMin);
|
|
encs.push(encodings.pseudoEncodingDynamicQualityMaxLevel0 + this.dynamicQualityMax);
|
|
encs.push(encodings.pseudoEncodingVideoAreaLevel1 + this.videoArea - 1);
|
|
encs.push(encodings.pseudoEncodingVideoTimeLevel0 + this.videoTime);
|
|
encs.push(encodings.pseudoEncodingVideoOutTimeLevel1 + this.videoOutTime - 1);
|
|
encs.push(encodings.pseudoEncodingVideoScalingLevel0 + this.videoScaling);
|
|
encs.push(encodings.pseudoEncodingFrameRateLevel10 + this.frameRate - 10);
|
|
encs.push(encodings.pseudoEncodingMaxVideoResolution);
|
|
if (this.preferBandwidth) // must be last - server processes in reverse order
|
|
encs.push(encodings.pseudoEncodingPreferBandwidth);
|
|
|
|
if (this._fb_depth == 24) {
|
|
encs.push(encodings.pseudoEncodingVMwareCursor);
|
|
encs.push(encodings.pseudoEncodingCursor);
|
|
}
|
|
|
|
if (supportsCursorURIs() && this._fb_depth == 24){
|
|
// Allow the user to attempt using a local cursor even if they are using a touch device. KASM-395
|
|
if (this.preferLocalCursor || !isTouchDevice){
|
|
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
|
|
*/
|
|
_init_msg() {
|
|
switch (this._rfb_init_state) {
|
|
case 'ProtocolVersion':
|
|
return this._negotiate_protocol_version();
|
|
|
|
case 'Security':
|
|
return this._negotiate_security();
|
|
|
|
case 'Authentication':
|
|
return this._negotiate_authentication();
|
|
|
|
case 'SecurityResult':
|
|
return this._handle_security_result();
|
|
|
|
case 'SecurityReason':
|
|
return this._handle_security_reason();
|
|
|
|
case 'ClientInitialisation':
|
|
this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation
|
|
this._rfb_init_state = 'ServerInitialisation';
|
|
return true;
|
|
|
|
case 'ServerInitialisation':
|
|
return this._negotiate_server_init();
|
|
|
|
default:
|
|
return this._fail("Unknown init state (state: " +
|
|
this._rfb_init_state + ")");
|
|
}
|
|
}
|
|
|
|
_handle_set_colour_map_msg() {
|
|
Log.Debug("SetColorMapEntries");
|
|
|
|
return this._fail("Unexpected SetColorMapEntries message");
|
|
}
|
|
|
|
_handle_server_cut_text() {
|
|
Log.Debug("ServerCutText");
|
|
|
|
if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
|
|
this._sock.rQskipBytes(3); // Padding
|
|
const length = this._sock.rQshift32();
|
|
if (this._sock.rQwait("ServerCutText", length, 8)) { return false; }
|
|
|
|
const text = this._sock.rQshiftStr(length);
|
|
|
|
if (this._viewOnly) { return true; }
|
|
|
|
this.dispatchEvent(new CustomEvent(
|
|
"clipboard",
|
|
{ detail: { text: text } }));
|
|
|
|
return true;
|
|
}
|
|
|
|
_handle_server_stats_msg() {
|
|
this._sock.rQskipBytes(3); // Padding
|
|
const length = this._sock.rQshift32();
|
|
if (this._sock.rQwait("KASM bottleneck stats", length, 8)) { return false; }
|
|
|
|
const text = this._sock.rQshiftStr(length);
|
|
|
|
console.log("Received KASM bottleneck stats:");
|
|
console.log(text);
|
|
this.dispatchEvent(new CustomEvent(
|
|
"bottleneck_stats",
|
|
{ detail: { text: text } }));
|
|
|
|
return true;
|
|
}
|
|
|
|
_handle_server_fence_msg() {
|
|
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;
|
|
}
|
|
|
|
_handle_xvp_msg() {
|
|
if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
|
|
this._sock.rQskipBytes(1); // Padding
|
|
const xvp_ver = this._sock.rQshift8();
|
|
const xvp_msg = this._sock.rQshift8();
|
|
|
|
switch (xvp_msg) {
|
|
case 0: // XVP_FAIL
|
|
Log.Error("XVP Operation Failed");
|
|
break;
|
|
case 1: // XVP_INIT
|
|
this._rfb_xvp_ver = xvp_ver;
|
|
Log.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")");
|
|
this._setCapability("power", true);
|
|
break;
|
|
default:
|
|
this._fail("Illegal server XVP message (msg: " + xvp_msg + ")");
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
_normal_msg() {
|
|
let msg_type;
|
|
if (this._FBU.rects > 0) {
|
|
msg_type = 0;
|
|
} else {
|
|
msg_type = this._sock.rQshift8();
|
|
}
|
|
|
|
let first, ret;
|
|
switch (msg_type) {
|
|
case 0: // FramebufferUpdate
|
|
ret = this._framebufferUpdate();
|
|
if (ret && !this._enabledContinuousUpdates) {
|
|
RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,
|
|
this._fb_width, this._fb_height);
|
|
}
|
|
return ret;
|
|
|
|
case 1: // SetColorMapEntries
|
|
return this._handle_set_colour_map_msg();
|
|
|
|
case 2: // Bell
|
|
Log.Debug("Bell");
|
|
this.dispatchEvent(new CustomEvent(
|
|
"bell",
|
|
{ detail: {} }));
|
|
return true;
|
|
|
|
case 3: // ServerCutText
|
|
return this._handle_server_cut_text();
|
|
|
|
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 178: // KASM bottleneck stats
|
|
return this._handle_server_stats_msg();
|
|
|
|
case 248: // ServerFence
|
|
return this._handle_server_fence_msg();
|
|
|
|
case 250: // XVP
|
|
return this._handle_xvp_msg();
|
|
|
|
default:
|
|
this._fail("Unexpected server message (type " + msg_type + ")");
|
|
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._handle_message();
|
|
}
|
|
}
|
|
|
|
_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;
|
|
}
|
|
|
|
if (document.visibilityState !== "hidden") {
|
|
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 cursor_type = this._sock.rQshift8();
|
|
|
|
this._sock.rQshift8(); //Padding
|
|
|
|
let rgba;
|
|
const bytesPerPixel = 4;
|
|
|
|
//Classic cursor
|
|
if (cursor_type == 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 and_mask = new Array(w * h);
|
|
for (let pixel = 0; pixel < (w * h); pixel++) {
|
|
and_mask[pixel] = this._sock.rQshift32();
|
|
}
|
|
|
|
let xor_mask = new Array(w * h);
|
|
for (let pixel = 0; pixel < (w * h); pixel++) {
|
|
xor_mask[pixel] = this._sock.rQshift32();
|
|
}
|
|
|
|
for (let pixel = 0; pixel < (w * h); pixel++) {
|
|
if (and_mask[pixel] == 0) {
|
|
//Fully opaque pixel
|
|
let bgr = xor_mask[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 ((and_mask[pixel] & PIXEL_MASK) ==
|
|
PIXEL_MASK) {
|
|
//Only screen value matters, no mouse colouring
|
|
if (xor_mask[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 ((xor_mask[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 xor_mask
|
|
rgba[(pixel * bytesPerPixel) ] = 0x00;
|
|
rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
|
|
rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
|
|
rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
|
|
}
|
|
|
|
} else {
|
|
//Unhandled and_mask
|
|
rgba[(pixel * bytesPerPixel) ] = 0x00;
|
|
rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
|
|
rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
|
|
rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
|
|
}
|
|
}
|
|
|
|
//Alpha cursor.
|
|
} else if (cursor_type == 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 >> 8 & 0xff; //r
|
|
rgba[(pixel * 4) + 1 ] = data >> 16 & 0xff; //g
|
|
rgba[(pixel * 4) + 2 ] = data >> 24 & 0xff; //b
|
|
rgba[(pixel * 4) + 3 ] = data & 0xff; //a
|
|
}
|
|
|
|
} else {
|
|
Log.Warn("The given cursor type is not supported: "
|
|
+ cursor_type + " 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 pix_idx = 0;
|
|
for (let y = 0; y < h; y++) {
|
|
for (let x = 0; x < w; x++) {
|
|
let mask_idx = y * Math.ceil(w / 8) + Math.floor(x / 8);
|
|
let alpha = (mask[mask_idx] << (x % 8)) & 0x80 ? 255 : 0;
|
|
rgba[pix_idx ] = pixels[pix_idx + 2];
|
|
rgba[pix_idx + 1] = pixels[pix_idx + 1];
|
|
rgba[pix_idx + 2] = pixels[pix_idx];
|
|
rgba[pix_idx + 3] = alpha;
|
|
pix_idx += 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);
|
|
|
|
this._setDesktopName(name);
|
|
|
|
return true;
|
|
}
|
|
|
|
_handleExtendedDesktopSize() {
|
|
if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
|
|
return false;
|
|
}
|
|
|
|
const number_of_screens = this._sock.rQpeek8();
|
|
|
|
let bytes = 4 + (number_of_screens * 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();
|
|
|
|
RFB.messages.setMaxVideoResolution(this._sock,
|
|
this._maxVideoResolutionX,
|
|
this._maxVideoResolutionY);
|
|
this.sentEventsCounter+=1;
|
|
}
|
|
|
|
this._sock.rQskipBytes(1); // number-of-screens
|
|
this._sock.rQskipBytes(3); // padding
|
|
|
|
for (let i = 0; i < number_of_screens; i += 1) {
|
|
// Save the id and flags of the first screen
|
|
if (i === 0) {
|
|
this._screen_id = 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._screen_flags = 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._fb_depth);
|
|
} catch (err) {
|
|
this._fail("Error decoding rect: " + err);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
_updateContinuousUpdates() {
|
|
if (!this._enabledContinuousUpdates) { return; }
|
|
|
|
RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
|
|
this._fb_width, this._fb_height);
|
|
}
|
|
|
|
_resize(width, height) {
|
|
this._fb_width = width;
|
|
this._fb_height = height;
|
|
|
|
this._display.resize(this._fb_width, this._fb_height);
|
|
|
|
// Adjust the visible viewport based on the new dimensions
|
|
this._updateClip();
|
|
this._updateScale();
|
|
|
|
this._updateContinuousUpdates();
|
|
}
|
|
|
|
_xvpOp(ver, op) {
|
|
if (this._rfb_xvp_ver < 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._rfb_connection_state !== '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(xt_scancode) {
|
|
const upperByte = (keycode >> 8);
|
|
const lowerByte = (keycode & 0x00ff);
|
|
if (upperByte === 0xe0 && lowerByte < 0x7f) {
|
|
return lowerByte | 0x80;
|
|
}
|
|
return xt_scancode;
|
|
}
|
|
|
|
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();
|
|
},
|
|
|
|
// TODO(directxman12): make this unicode compatible?
|
|
clientCutText(sock, text) {
|
|
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 = text.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 text we begin creating the
|
|
// buffer for the flush in the next iteration.
|
|
let textOffset = 0;
|
|
|
|
let remaining = length;
|
|
while (remaining > 0) {
|
|
|
|
let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen));
|
|
for (let i = 0; i < flushSize; i++) {
|
|
buff[sock._sQlen + i] = text.charCodeAt(textOffset + i);
|
|
}
|
|
|
|
sock._sQlen += flushSize;
|
|
sock.flush();
|
|
|
|
remaining -= flushSize;
|
|
textOffset += 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();
|
|
},
|
|
|
|
setMaxVideoResolution(sock, width, height) {
|
|
const buff = sock._sQ;
|
|
const offset = sock._sQlen;
|
|
|
|
buff[offset] = 252; // msg-type
|
|
buff[offset + 1] = width >> 8; // width
|
|
buff[offset + 2] = width;
|
|
buff[offset + 3] = height >> 8; // height
|
|
buff[offset + 4] = height;
|
|
|
|
sock._sQlen += 5;
|
|
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();
|
|
},
|
|
|
|
requestStats(sock) {
|
|
const buff = sock._sQ;
|
|
const offset = sock._sQlen;
|
|
|
|
if (buff == null) { return; }
|
|
|
|
buff[offset] = 178; // msg-type
|
|
|
|
buff[offset + 1] = 0; // padding
|
|
buff[offset + 2] = 0; // padding
|
|
buff[offset + 3] = 0; // padding
|
|
|
|
sock._sQlen += 4;
|
|
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, true_color) {
|
|
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] = true_color ? 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 * 2; // red-shift
|
|
buff[offset + 15] = bits * 1; // green-shift
|
|
buff[offset + 16] = bits * 0; // 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,
|
|
}
|
|
};
|