JavaScript 65.5%
Python 17.8%
CSS 13%
HTML 3.7%
1/*2 * noVNC: HTML5 VNC client3 * Copyright (C) 2020 The noVNC authors4 * Licensed under MPL 2.0 (see LICENSE.txt)5 *6 * See README.md for usage and integration instructions.7 *8 */910import { toUnsigned32bit, toSigned32bit } from './util/int.js';11import * as Log from './util/logging.js';12import { encodeUTF8, decodeUTF8 } from './util/strings.js';13import { dragThreshold, supportsWebCodecsH264Decode } from './util/browser.js';14import { clientToElement } from './util/element.js';15import { setCapture } from './util/events.js';16import EventTargetMixin from './util/eventtarget.js';17import Display from "./display.js";18import Inflator from "./inflator.js";19import Deflator from "./deflator.js";20import Keyboard from "./input/keyboard.js";21import GestureHandler from "./input/gesturehandler.js";22import Cursor from "./util/cursor.js";23import Websock from "./websock.js";24import KeyTable from "./input/keysym.js";25import XtScancode from "./input/xtscancodes.js";26import { encodings } from "./encodings.js";27import RSAAESAuthenticationState from "./ra2.js";28import legacyCrypto from "./crypto/crypto.js";2930import RawDecoder from "./decoders/raw.js";31import CopyRectDecoder from "./decoders/copyrect.js";32import RREDecoder from "./decoders/rre.js";33import HextileDecoder from "./decoders/hextile.js";34import ZlibDecoder from './decoders/zlib.js';35import TightDecoder from "./decoders/tight.js";36import TightPNGDecoder from "./decoders/tightpng.js";37import ZRLEDecoder from "./decoders/zrle.js";38import JPEGDecoder from "./decoders/jpeg.js";39import H264Decoder from "./decoders/h264.js";4041// How many seconds to wait for a disconnect to finish42const DISCONNECT_TIMEOUT = 3;43const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';4445// Minimum wait (ms) between two mouse moves46const MOUSE_MOVE_DELAY = 17;4748// Wheel thresholds49const WHEEL_STEP = 50; // Pixels needed for one step50const WHEEL_LINE_HEIGHT = 19; // Assumed pixels for one line step5152// Gesture thresholds53const GESTURE_ZOOMSENS = 75;54const GESTURE_SCRLSENS = 50;55const DOUBLE_TAP_TIMEOUT = 1000;56const DOUBLE_TAP_THRESHOLD = 50;5758// Security types59const securityTypeNone = 1;60const securityTypeVNCAuth = 2;61const securityTypeRA2ne = 6;62const securityTypeTight = 16;63const securityTypeVeNCrypt = 19;64const securityTypeXVP = 22;65const securityTypeARD = 30;66const securityTypeMSLogonII = 113;6768// Special Tight security types69const securityTypeUnixLogon = 129;7071// VeNCrypt security types72const securityTypePlain = 256;7374// Extended clipboard pseudo-encoding formats75const extendedClipboardFormatText = 1;76/*eslint-disable no-unused-vars */77const extendedClipboardFormatRtf = 1 << 1;78const extendedClipboardFormatHtml = 1 << 2;79const extendedClipboardFormatDib = 1 << 3;80const extendedClipboardFormatFiles = 1 << 4;81/*eslint-enable */8283// Extended clipboard pseudo-encoding actions84const extendedClipboardActionCaps = 1 << 24;85const extendedClipboardActionRequest = 1 << 25;86const extendedClipboardActionPeek = 1 << 26;87const extendedClipboardActionNotify = 1 << 27;88const extendedClipboardActionProvide = 1 << 28;8990export default class RFB extends EventTargetMixin {91 constructor(target, urlOrChannel, options) {92 if (!target) {93 throw new Error("Must specify target");94 }95 if (!urlOrChannel) {96 throw new Error("Must specify URL, WebSocket or RTCDataChannel");97 }9899 // We rely on modern APIs which might not be available in an100 // insecure context101 if (!window.isSecureContext) {102 Log.Error("noVNC requires a secure context (TLS). Expect crashes!");103 }104105 super();106107 this._target = target;108109 if (typeof urlOrChannel === "string") {110 this._url = urlOrChannel;111 } else {112 this._url = null;113 this._rawChannel = urlOrChannel;114 }115116 // Connection details117 options = options || {};118 this._rfbCredentials = options.credentials || {};119 this._shared = 'shared' in options ? !!options.shared : true;120 this._repeaterID = options.repeaterID || '';121 this._wsProtocols = options.wsProtocols || [];122123 // Internal state124 this._rfbConnectionState = '';125 this._rfbInitState = '';126 this._rfbAuthScheme = -1;127 this._rfbCleanDisconnect = true;128 this._rfbRSAAESAuthenticationState = null;129130 // Server capabilities131 this._rfbVersion = 0;132 this._rfbMaxVersion = 3.8;133 this._rfbTightVNC = false;134 this._rfbVeNCryptState = 0;135 this._rfbXvpVer = 0;136137 this._fbWidth = 0;138 this._fbHeight = 0;139140 this._fbName = "";141142 this._capabilities = { power: false };143144 this._supportsFence = false;145146 this._supportsContinuousUpdates = false;147 this._enabledContinuousUpdates = false;148149 this._supportsSetDesktopSize = false;150 this._screenID = 0;151 this._screenFlags = 0;152 this._pendingRemoteResize = false;153 this._lastResize = 0;154155 this._qemuExtKeyEventSupported = false;156157 this._extendedPointerEventSupported = false;158159 this._clipboardText = null;160 this._clipboardServerCapabilitiesActions = {};161 this._clipboardServerCapabilitiesFormats = {};162163 // Internal objects164 this._sock = null; // Websock object165 this._display = null; // Display object166 this._flushing = false; // Display flushing state167 this._keyboard = null; // Keyboard input handler object168 this._gestures = null; // Gesture input handler object169 this._resizeObserver = null; // Resize observer object170171 // Timers172 this._disconnTimer = null; // disconnection timer173 this._resizeTimeout = null; // resize rate limiting174 this._mouseMoveTimer = null;175176 // Decoder states177 this._decoders = {};178179 this._FBU = {180 rects: 0,181 x: 0,182 y: 0,183 width: 0,184 height: 0,185 encoding: null,186 };187188 // Mouse state189 this._mousePos = {};190 this._mouseButtonMask = 0;191 this._mouseLastMoveTime = 0;192 this._viewportDragging = false;193 this._viewportDragPos = {};194 this._viewportHasMoved = false;195 this._accumulatedWheelDeltaX = 0;196 this._accumulatedWheelDeltaY = 0;197198 // Gesture state199 this._gestureLastTapTime = null;200 this._gestureFirstDoubleTapEv = null;201 this._gestureLastMagnitudeX = 0;202 this._gestureLastMagnitudeY = 0;203204 // Bound event handlers205 this._eventHandlers = {206 focusCanvas: this._focusCanvas.bind(this),207 handleResize: this._handleResize.bind(this),208 handleMouse: this._handleMouse.bind(this),209 handleWheel: this._handleWheel.bind(this),210 handleGesture: this._handleGesture.bind(this),211 handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this),212 handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this),213 };214215 // main setup216 Log.Debug(">> RFB.constructor");217218 // Create DOM elements219 this._screen = document.createElement('div');220 this._screen.style.display = 'flex';221 this._screen.style.width = '100%';222 this._screen.style.height = '100%';223 this._screen.style.overflow = 'auto';224 this._screen.style.background = DEFAULT_BACKGROUND;225 this._canvas = document.createElement('canvas');226 this._canvas.style.margin = 'auto';227 // Some browsers add an outline on focus228 this._canvas.style.outline = 'none';229 this._canvas.width = 0;230 this._canvas.height = 0;231 this._canvas.tabIndex = -1;232 this._screen.appendChild(this._canvas);233234 // Cursor235 this._cursor = new Cursor();236237 // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes238 // it. Result: no cursor at all until a window border or an edit field239 // is hit blindly. But there are also VNC servers that draw the cursor240 // in the framebuffer and don't send the empty local cursor. There is241 // no way to satisfy both sides.242 //243 // The spec is unclear on this "initial cursor" issue. Many other244 // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the245 // initial cursor instead.246 this._cursorImage = RFB.cursors.none;247248 // populate decoder array with objects249 this._decoders[encodings.encodingRaw] = new RawDecoder();250 this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();251 this._decoders[encodings.encodingRRE] = new RREDecoder();252 this._decoders[encodings.encodingHextile] = new HextileDecoder();253 this._decoders[encodings.encodingZlib] = new ZlibDecoder();254 this._decoders[encodings.encodingTight] = new TightDecoder();255 this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();256 this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();257 this._decoders[encodings.encodingJPEG] = new JPEGDecoder();258 this._decoders[encodings.encodingH264] = new H264Decoder();259260 // NB: nothing that needs explicit teardown should be done261 // before this point, since this can throw an exception262 try {263 this._display = new Display(this._canvas);264 } catch (exc) {265 Log.Error("Display exception: " + exc);266 throw exc;267 }268269 this._keyboard = new Keyboard(this._canvas);270 this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);271 this._remoteCapsLock = null; // Null indicates unknown or irrelevant272 this._remoteNumLock = null;273274 this._gestures = new GestureHandler();275276 this._sock = new Websock();277 this._sock.on('open', this._socketOpen.bind(this));278 this._sock.on('close', this._socketClose.bind(this));279 this._sock.on('message', this._handleMessage.bind(this));280 this._sock.on('error', this._socketError.bind(this));281282 this._expectedClientWidth = null;283 this._expectedClientHeight = null;284 this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize);285286 // All prepared, kick off the connection287 this._updateConnectionState('connecting');288289 Log.Debug("<< RFB.constructor");290291 // ===== PROPERTIES =====292293 this.dragViewport = false;294 this.focusOnClick = true;295296 this._viewOnly = false;297 this._clipViewport = false;298 this._clippingViewport = false;299 this._scaleViewport = false;300 this._resizeSession = false;301302 this._showDotCursor = false;303 if (options.showDotCursor !== undefined) {304 Log.Warn("Specifying showDotCursor as a RFB constructor argument is deprecated");305 this._showDotCursor = options.showDotCursor;306 }307308 this._qualityLevel = 6;309 this._compressionLevel = 2;310 }311312 // ===== PROPERTIES =====313314 get viewOnly() { return this._viewOnly; }315 set viewOnly(viewOnly) {316 this._viewOnly = viewOnly;317318 if (this._rfbConnectionState === "connecting" ||319 this._rfbConnectionState === "connected") {320 if (viewOnly) {321 this._keyboard.ungrab();322 } else {323 this._keyboard.grab();324 }325 }326 }327328 get capabilities() { return this._capabilities; }329330 get clippingViewport() { return this._clippingViewport; }331 _setClippingViewport(on) {332 if (on === this._clippingViewport) {333 return;334 }335 this._clippingViewport = on;336 this.dispatchEvent(new CustomEvent("clippingviewport",337 { detail: this._clippingViewport }));338 }339340 get touchButton() { return 0; }341 set touchButton(button) { Log.Warn("Using old API!"); }342343 get clipViewport() { return this._clipViewport; }344 set clipViewport(viewport) {345 this._clipViewport = viewport;346 this._updateClip();347 }348349 get scaleViewport() { return this._scaleViewport; }350 set scaleViewport(scale) {351 this._scaleViewport = scale;352 // Scaling trumps clipping, so we may need to adjust353 // clipping when enabling or disabling scaling354 if (scale && this._clipViewport) {355 this._updateClip();356 }357 this._updateScale();358 if (!scale && this._clipViewport) {359 this._updateClip();360 }361 }362363 get resizeSession() { return this._resizeSession; }364 set resizeSession(resize) {365 this._resizeSession = resize;366 if (resize) {367 this._requestRemoteResize();368 }369 }370371 get showDotCursor() { return this._showDotCursor; }372 set showDotCursor(show) {373 this._showDotCursor = show;374 this._refreshCursor();375 }376377 get background() { return this._screen.style.background; }378 set background(cssValue) { this._screen.style.background = cssValue; }379380 get qualityLevel() {381 return this._qualityLevel;382 }383 set qualityLevel(qualityLevel) {384 if (!Number.isInteger(qualityLevel) || qualityLevel < 0 || qualityLevel > 9) {385 Log.Error("qualityLevel must be an integer between 0 and 9");386 return;387 }388389 if (this._qualityLevel === qualityLevel) {390 return;391 }392393 this._qualityLevel = qualityLevel;394395 if (this._rfbConnectionState === 'connected') {396 this._sendEncodings();397 }398 }399400 get compressionLevel() {401 return this._compressionLevel;402 }403 set compressionLevel(compressionLevel) {404 if (!Number.isInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) {405 Log.Error("compressionLevel must be an integer between 0 and 9");406 return;407 }408409 if (this._compressionLevel === compressionLevel) {410 return;411 }412413 this._compressionLevel = compressionLevel;414415 if (this._rfbConnectionState === 'connected') {416 this._sendEncodings();417 }418 }419420 // ===== PUBLIC METHODS =====421422 disconnect() {423 this._updateConnectionState('disconnecting');424 this._sock.off('error');425 this._sock.off('message');426 this._sock.off('open');427 if (this._rfbRSAAESAuthenticationState !== null) {428 this._rfbRSAAESAuthenticationState.disconnect();429 }430 }431432 approveServer() {433 if (this._rfbRSAAESAuthenticationState !== null) {434 this._rfbRSAAESAuthenticationState.approveServer();435 }436 }437438 sendCredentials(creds) {439 this._rfbCredentials = creds;440 this._resumeAuthentication();441 }442443 sendCtrlAltDel() {444 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }445 Log.Info("Sending Ctrl-Alt-Del");446447 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);448 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);449 this.sendKey(KeyTable.XK_Delete, "Delete", true);450 this.sendKey(KeyTable.XK_Delete, "Delete", false);451 this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);452 this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);453 }454455 machineShutdown() {456 this._xvpOp(1, 2);457 }458459 machineReboot() {460 this._xvpOp(1, 3);461 }462463 machineReset() {464 this._xvpOp(1, 4);465 }466467 // Send a key press. If 'down' is not specified then send a down key468 // followed by an up key.469 sendKey(keysym, code, down) {470 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }471472 if (down === undefined) {473 this.sendKey(keysym, code, true);474 this.sendKey(keysym, code, false);475 return;476 }477478 const scancode = XtScancode[code];479480 if (this._qemuExtKeyEventSupported && scancode) {481 // 0 is NoSymbol482 keysym = keysym || 0;483484 Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);485486 RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);487 } else {488 if (!keysym) {489 return;490 }491 Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);492 RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);493 }494 }495496 focus(options) {497 this._canvas.focus(options);498 }499500 blur() {501 this._canvas.blur();502 }503504 clipboardPasteFrom(text) {505 if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }506507 if (this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] &&508 this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {509510 this._clipboardText = text;511 RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);512 } else {513 let length, i;514 let data;515516 length = 0;517 // eslint-disable-next-line no-unused-vars518 for (let codePoint of text) {519 length++;520 }521522 data = new Uint8Array(length);523524 i = 0;525 for (let codePoint of text) {526 let code = codePoint.codePointAt(0);527528 /* Only ISO 8859-1 is supported */529 if (code > 0xff) {530 code = 0x3f; // '?'531 }532533 data[i++] = code;534 }535536 RFB.messages.clientCutText(this._sock, data);537 }538 }539540 getImageData() {541 return this._display.getImageData();542 }543544 toDataURL(type, encoderOptions) {545 return this._display.toDataURL(type, encoderOptions);546 }547548 toBlob(callback, type, quality) {549 return this._display.toBlob(callback, type, quality);550 }551552 // ===== PRIVATE METHODS =====553554 _connect() {555 Log.Debug(">> RFB.connect");556557 if (this._url) {558 Log.Info(`connecting to ${this._url}`);559 this._sock.open(this._url, this._wsProtocols);560 } else {561 Log.Info(`attaching ${this._rawChannel} to Websock`);562 this._sock.attach(this._rawChannel);563564 if (this._sock.readyState === 'closed') {565 throw Error("Cannot use already closed WebSocket/RTCDataChannel");566 }567568 if (this._sock.readyState === 'open') {569 // FIXME: _socketOpen() can in theory call _fail(), which570 // isn't allowed this early, but I'm not sure that can571 // happen without a bug messing up our state variables572 this._socketOpen();573 }574 }575576 // Make our elements part of the page577 this._target.appendChild(this._screen);578579 this._gestures.attach(this._canvas);580581 this._cursor.attach(this._canvas);582 this._refreshCursor();583584 // Monitor size changes of the screen element585 this._resizeObserver.observe(this._screen);586587 // Always grab focus on some kind of click event588 this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);589 this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);590591 // Mouse events592 this._canvas.addEventListener('mousedown', this._eventHandlers.handleMouse);593 this._canvas.addEventListener('mouseup', this._eventHandlers.handleMouse);594 this._canvas.addEventListener('mousemove', this._eventHandlers.handleMouse);595 // Prevent middle-click pasting (see handler for why we bind to document)596 this._canvas.addEventListener('click', this._eventHandlers.handleMouse);597 // preventDefault() on mousedown doesn't stop this event for some598 // reason so we have to explicitly block it599 this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);600601 // Wheel events602 this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);603604 // Gesture events605 this._canvas.addEventListener("gesturestart", this._eventHandlers.handleGesture);606 this._canvas.addEventListener("gesturemove", this._eventHandlers.handleGesture);607 this._canvas.addEventListener("gestureend", this._eventHandlers.handleGesture);608609 Log.Debug("<< RFB.connect");610 }611612 _disconnect() {613 Log.Debug(">> RFB.disconnect");614 this._cursor.detach();615 this._canvas.removeEventListener("gesturestart", this._eventHandlers.handleGesture);616 this._canvas.removeEventListener("gesturemove", this._eventHandlers.handleGesture);617 this._canvas.removeEventListener("gestureend", this._eventHandlers.handleGesture);618 this._canvas.removeEventListener("wheel", this._eventHandlers.handleWheel);619 this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse);620 this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse);621 this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);622 this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);623 this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);624 this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);625 this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);626 this._resizeObserver.disconnect();627 this._keyboard.ungrab();628 this._gestures.detach();629 this._sock.close();630 try {631 this._target.removeChild(this._screen);632 } catch (e) {633 if (e.name === 'NotFoundError') {634 // Some cases where the initial connection fails635 // can disconnect before the _screen is created636 } else {637 throw e;638 }639 }640 clearTimeout(this._resizeTimeout);641 clearTimeout(this._mouseMoveTimer);642 Log.Debug("<< RFB.disconnect");643 }644645 _socketOpen() {646 if ((this._rfbConnectionState === 'connecting') &&647 (this._rfbInitState === '')) {648 this._rfbInitState = 'ProtocolVersion';649 Log.Debug("Starting VNC handshake");650 } else {651 this._fail("Unexpected server connection while " +652 this._rfbConnectionState);653 }654 }655656 _socketClose(e) {657 Log.Debug("WebSocket on-close event");658 let msg = "";659 if (e.code) {660 msg = "(code: " + e.code;661 if (e.reason) {662 msg += ", reason: " + e.reason;663 }664 msg += ")";665 }666 switch (this._rfbConnectionState) {667 case 'connecting':668 this._fail("Connection closed " + msg);669 break;670 case 'connected':671 // Handle disconnects that were initiated server-side672 this._updateConnectionState('disconnecting');673 this._updateConnectionState('disconnected');674 break;675 case 'disconnecting':676 // Normal disconnection path677 this._updateConnectionState('disconnected');678 break;679 case 'disconnected':680 this._fail("Unexpected server disconnect " +681 "when already disconnected " + msg);682 break;683 default:684 this._fail("Unexpected server disconnect before connecting " +685 msg);686 break;687 }688 this._sock.off('close');689 // Delete reference to raw channel to allow cleanup.690 this._rawChannel = null;691 }692693 _socketError(e) {694 Log.Warn("WebSocket on-error event");695 }696697 _focusCanvas(event) {698 if (!this.focusOnClick) {699 return;700 }701702 this.focus({ preventScroll: true });703 }704705 _setDesktopName(name) {706 this._fbName = name;707 this.dispatchEvent(new CustomEvent(708 "desktopname",709 { detail: { name: this._fbName } }));710 }711712 _saveExpectedClientSize() {713 this._expectedClientWidth = this._screen.clientWidth;714 this._expectedClientHeight = this._screen.clientHeight;715 }716717 _currentClientSize() {718 return [this._screen.clientWidth, this._screen.clientHeight];719 }720721 _clientHasExpectedSize() {722 const [currentWidth, currentHeight] = this._currentClientSize();723 return currentWidth == this._expectedClientWidth &&724 currentHeight == this._expectedClientHeight;725 }726727 // Handle browser window resizes728 _handleResize() {729 // Don't change anything if the client size is already as expected730 if (this._clientHasExpectedSize()) {731 return;732 }733 // If the window resized then our screen element might have734 // as well. Update the viewport dimensions.735 window.requestAnimationFrame(() => {736 this._updateClip();737 this._updateScale();738 this._saveExpectedClientSize();739 });740741 // Request changing the resolution of the remote display to742 // the size of the local browser viewport.743 this._requestRemoteResize();744 }745746 // Update state of clipping in Display object, and make sure the747 // configured viewport matches the current screen size748 _updateClip() {749 const curClip = this._display.clipViewport;750 let newClip = this._clipViewport;751752 if (this._scaleViewport) {753 // Disable viewport clipping if we are scaling754 newClip = false;755 }756757 if (curClip !== newClip) {758 this._display.clipViewport = newClip;759 }760761 if (newClip) {762 // When clipping is enabled, the screen is limited to763 // the size of the container.764 const size = this._screenSize();765 this._display.viewportChangeSize(size.w, size.h);766 this._fixScrollbars();767 this._setClippingViewport(size.w < this._display.width ||768 size.h < this._display.height);769 } else {770 this._setClippingViewport(false);771 }772773 // When changing clipping we might show or hide scrollbars.774 // This causes the expected client dimensions to change.775 if (curClip !== newClip) {776 this._saveExpectedClientSize();777 }778 }779780 _updateScale() {781 if (!this._scaleViewport) {782 this._display.scale = 1.0;783 } else {784 const size = this._screenSize();785 this._display.autoscale(size.w, size.h);786 }787 this._fixScrollbars();788 }789790 // Requests a change of remote desktop size. This message is an extension791 // and may only be sent if we have received an ExtendedDesktopSize message792 _requestRemoteResize() {793 if (!this._resizeSession) {794 return;795 }796 if (this._viewOnly) {797 return;798 }799 if (!this._supportsSetDesktopSize) {800 return;801 }802803 // Rate limit to one pending resize at a time804 if (this._pendingRemoteResize) {805 return;806 }807808 // And no more than once every 100ms809 if ((Date.now() - this._lastResize) < 100) {810 clearTimeout(this._resizeTimeout);811 this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this),812 100 - (Date.now() - this._lastResize));813 return;814 }815 this._resizeTimeout = null;816817 const size = this._screenSize();818819 // Do we actually change anything?820 if (size.w === this._fbWidth && size.h === this._fbHeight) {821 return;822 }823824 this._pendingRemoteResize = true;825 this._lastResize = Date.now();826 RFB.messages.setDesktopSize(this._sock,827 Math.floor(size.w), Math.floor(size.h),828 this._screenID, this._screenFlags);829830 Log.Debug('Requested new desktop size: ' +831 size.w + 'x' + size.h);832 }833834 // Gets the the size of the available screen835 _screenSize() {836 let r = this._screen.getBoundingClientRect();837 return { w: r.width, h: r.height };838 }839840 _fixScrollbars() {841 // This is a hack because Safari on macOS screws up the calculation842 // for when scrollbars are needed. We get scrollbars when making the843 // browser smaller, despite remote resize being enabled. So to fix it844 // we temporarily toggle them off and on.845 const orig = this._screen.style.overflow;846 this._screen.style.overflow = 'hidden';847 // Force Safari to recalculate the layout by asking for848 // an element's dimensions849 this._screen.getBoundingClientRect();850 this._screen.style.overflow = orig;851 }852853 /*854 * Connection states:855 * connecting856 * connected857 * disconnecting858 * disconnected - permanent state859 */860 _updateConnectionState(state) {861 const oldstate = this._rfbConnectionState;862863 if (state === oldstate) {864 Log.Debug("Already in state '" + state + "', ignoring");865 return;866 }867868 // The 'disconnected' state is permanent for each RFB object869 if (oldstate === 'disconnected') {870 Log.Error("Tried changing state of a disconnected RFB object");871 return;872 }873874 // Ensure proper transitions before doing anything875 switch (state) {876 case 'connected':877 if (oldstate !== 'connecting') {878 Log.Error("Bad transition to connected state, " +879 "previous connection state: " + oldstate);880 return;881 }882 break;883884 case 'disconnected':885 if (oldstate !== 'disconnecting') {886 Log.Error("Bad transition to disconnected state, " +887 "previous connection state: " + oldstate);888 return;889 }890 break;891892 case 'connecting':893 if (oldstate !== '') {894 Log.Error("Bad transition to connecting state, " +895 "previous connection state: " + oldstate);896 return;897 }898 break;899900 case 'disconnecting':901 if (oldstate !== 'connected' && oldstate !== 'connecting') {902 Log.Error("Bad transition to disconnecting state, " +903 "previous connection state: " + oldstate);904 return;905 }906 break;907908 default:909 Log.Error("Unknown connection state: " + state);910 return;911 }912913 // State change actions914915 this._rfbConnectionState = state;916917 Log.Debug("New state '" + state + "', was '" + oldstate + "'.");918919 if (this._disconnTimer && state !== 'disconnecting') {920 Log.Debug("Clearing disconnect timer");921 clearTimeout(this._disconnTimer);922 this._disconnTimer = null;923924 // make sure we don't get a double event925 this._sock.off('close');926 }927928 switch (state) {929 case 'connecting':930 this._connect();931 break;932933 case 'connected':934 this.dispatchEvent(new CustomEvent("connect", { detail: {} }));935 break;936937 case 'disconnecting':938 this._disconnect();939940 this._disconnTimer = setTimeout(() => {941 Log.Error("Disconnection timed out.");942 this._updateConnectionState('disconnected');943 }, DISCONNECT_TIMEOUT * 1000);944 break;945946 case 'disconnected':947 this.dispatchEvent(new CustomEvent(948 "disconnect", { detail:949 { clean: this._rfbCleanDisconnect } }));950 break;951 }952 }953954 /* Print errors and disconnect955 *956 * The parameter 'details' is used for information that957 * should be logged but not sent to the user interface.958 */959 _fail(details) {960 switch (this._rfbConnectionState) {961 case 'disconnecting':962 Log.Error("Failed when disconnecting: " + details);963 break;964 case 'connected':965 Log.Error("Failed while connected: " + details);966 break;967 case 'connecting':968 Log.Error("Failed when connecting: " + details);969 break;970 default:971 Log.Error("RFB failure: " + details);972 break;973 }974 this._rfbCleanDisconnect = false; //This is sent to the UI975976 // Transition to disconnected without waiting for socket to close977 this._updateConnectionState('disconnecting');978 this._updateConnectionState('disconnected');979980 return false;981 }982983 _setCapability(cap, val) {984 this._capabilities[cap] = val;985 this.dispatchEvent(new CustomEvent("capabilities",986 { detail: { capabilities: this._capabilities } }));987 }988989 _handleMessage() {990 if (this._sock.rQwait("message", 1)) {991 Log.Warn("handleMessage called on an empty receive queue");992 return;993 }994995 switch (this._rfbConnectionState) {996 case 'disconnected':997 Log.Error("Got data while disconnected");998 break;999 case 'connected':1000 while (true) {1001 if (this._flushing) {1002 break;1003 }1004 if (!this._normalMsg()) {1005 break;1006 }1007 if (this._sock.rQwait("message", 1)) {1008 break;1009 }1010 }1011 break;1012 case 'connecting':1013 while (this._rfbConnectionState === 'connecting') {1014 if (!this._initMsg()) {1015 break;1016 }1017 }1018 break;1019 default:1020 Log.Error("Got data while in an invalid state");1021 break;1022 }1023 }10241025 _handleKeyEvent(keysym, code, down, numlock, capslock) {1026 // If remote state of capslock is known, and it doesn't match the local led state of1027 // the keyboard, we send a capslock keypress first to bring it into sync.1028 // If we just pressed CapsLock, or we toggled it remotely due to it being out of sync1029 // we clear the remote state so that we don't send duplicate or spurious fixes,1030 // since it may take some time to receive the new remote CapsLock state.1031 if (code == 'CapsLock' && down) {1032 this._remoteCapsLock = null;1033 }1034 if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) {1035 Log.Debug("Fixing remote caps lock");10361037 this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true);1038 this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false);1039 // We clear the remote capsLock state when we do this to prevent issues with doing this twice1040 // before we receive an update of the the remote state.1041 this._remoteCapsLock = null;1042 }10431044 // Logic for numlock is exactly the same.1045 if (code == 'NumLock' && down) {1046 this._remoteNumLock = null;1047 }1048 if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) {1049 Log.Debug("Fixing remote num lock");1050 this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true);1051 this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false);1052 this._remoteNumLock = null;1053 }1054 this.sendKey(keysym, code, down);1055 }10561057 static _convertButtonMask(buttons) {1058 /* The bits in MouseEvent.buttons property correspond1059 * to the following mouse buttons:1060 * 0: Left1061 * 1: Right1062 * 2: Middle1063 * 3: Back1064 * 4: Forward1065 *1066 * These bits needs to be converted to what they are defined as1067 * in the RFB protocol.1068 */10691070 const buttonMaskMap = {1071 0: 1 << 0, // Left1072 1: 1 << 2, // Right1073 2: 1 << 1, // Middle1074 3: 1 << 7, // Back1075 4: 1 << 8, // Forward1076 };10771078 let bmask = 0;1079 for (let i = 0; i < 5; i++) {1080 if (buttons & (1 << i)) {1081 bmask |= buttonMaskMap[i];1082 }1083 }1084 return bmask;1085 }10861087 _handleMouse(ev) {1088 /*1089 * We don't check connection status or viewOnly here as the1090 * mouse events might be used to control the viewport1091 */10921093 if (ev.type === 'click') {1094 /*1095 * Note: This is only needed for the 'click' event as it fails1096 * to fire properly for the target element so we have1097 * to listen on the document element instead.1098 */1099 if (ev.target !== this._canvas) {1100 return;1101 }1102 }11031104 // FIXME: if we're in view-only and not dragging,1105 // should we stop events?1106 ev.stopPropagation();1107 ev.preventDefault();11081109 if ((ev.type === 'click') || (ev.type === 'contextmenu')) {1110 return;1111 }11121113 let pos = clientToElement(ev.clientX, ev.clientY,1114 this._canvas);11151116 let bmask = RFB._convertButtonMask(ev.buttons);11171118 let down = ev.type == 'mousedown';1119 switch (ev.type) {1120 case 'mousedown':1121 case 'mouseup':1122 if (this.dragViewport) {1123 if (down && !this._viewportDragging) {1124 this._viewportDragging = true;1125 this._viewportDragPos = {'x': pos.x, 'y': pos.y};1126 this._viewportHasMoved = false;11271128 this._flushMouseMoveTimer(pos.x, pos.y);11291130 // Skip sending mouse events, instead save the current1131 // mouse mask so we can send it later.1132 this._mouseButtonMask = bmask;1133 break;1134 } else {1135 this._viewportDragging = false;11361137 // If we actually performed a drag then we are done1138 // here and should not send any mouse events1139 if (this._viewportHasMoved) {1140 this._mouseButtonMask = bmask;1141 break;1142 }1143 // Otherwise we treat this as a mouse click event.1144 // Send the previously saved button mask, followed1145 // by the current button mask at the end of this1146 // function.1147 this._sendMouse(pos.x, pos.y, this._mouseButtonMask);1148 }1149 }1150 if (down) {1151 setCapture(this._canvas);1152 }1153 this._handleMouseButton(pos.x, pos.y, bmask);1154 break;1155 case 'mousemove':1156 if (this._viewportDragging) {1157 const deltaX = this._viewportDragPos.x - pos.x;1158 const deltaY = this._viewportDragPos.y - pos.y;11591160 if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||1161 Math.abs(deltaY) > dragThreshold)) {1162 this._viewportHasMoved = true;11631164 this._viewportDragPos = {'x': pos.x, 'y': pos.y};1165 this._display.viewportChangePos(deltaX, deltaY);1166 }11671168 // Skip sending mouse events1169 break;1170 }1171 this._handleMouseMove(pos.x, pos.y);1172 break;1173 }1174 }11751176 _handleMouseButton(x, y, bmask) {1177 // Flush waiting move event first1178 this._flushMouseMoveTimer(x, y);11791180 this._mouseButtonMask = bmask;1181 this._sendMouse(x, y, this._mouseButtonMask);1182 }11831184 _handleMouseMove(x, y) {1185 this._mousePos = { 'x': x, 'y': y };11861187 // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms1188 if (this._mouseMoveTimer == null) {11891190 const timeSinceLastMove = Date.now() - this._mouseLastMoveTime;1191 if (timeSinceLastMove > MOUSE_MOVE_DELAY) {1192 this._sendMouse(x, y, this._mouseButtonMask);1193 this._mouseLastMoveTime = Date.now();1194 } else {1195 // Too soon since the latest move, wait the remaining time1196 this._mouseMoveTimer = setTimeout(() => {1197 this._handleDelayedMouseMove();1198 }, MOUSE_MOVE_DELAY - timeSinceLastMove);1199 }1200 }1201 }12021203 _handleDelayedMouseMove() {1204 this._mouseMoveTimer = null;1205 this._sendMouse(this._mousePos.x, this._mousePos.y,1206 this._mouseButtonMask);1207 this._mouseLastMoveTime = Date.now();1208 }12091210 _sendMouse(x, y, mask) {1211 if (this._rfbConnectionState !== 'connected') { return; }1212 if (this._viewOnly) { return; } // View only, skip mouse events12131214 // Highest bit in mask is never sent to the server1215 if (mask & 0x8000) {1216 throw new Error("Illegal mouse button mask (mask: " + mask + ")");1217 }12181219 let extendedMouseButtons = mask & 0x7f80;12201221 if (this._extendedPointerEventSupported && extendedMouseButtons) {1222 RFB.messages.extendedPointerEvent(this._sock, this._display.absX(x),1223 this._display.absY(y), mask);1224 } else {1225 RFB.messages.pointerEvent(this._sock, this._display.absX(x),1226 this._display.absY(y), mask);1227 }1228 }12291230 _handleWheel(ev) {1231 if (this._rfbConnectionState !== 'connected') { return; }1232 if (this._viewOnly) { return; } // View only, skip mouse events12331234 ev.stopPropagation();1235 ev.preventDefault();12361237 let pos = clientToElement(ev.clientX, ev.clientY,1238 this._canvas);12391240 let bmask = RFB._convertButtonMask(ev.buttons);1241 let dX = ev.deltaX;1242 let dY = ev.deltaY;12431244 // Pixel units unless it's non-zero.1245 // Note that if deltamode is line or page won't matter since we aren't1246 // sending the mouse wheel delta to the server anyway.1247 // The difference between pixel and line can be important however since1248 // we have a threshold that can be smaller than the line height.1249 if (ev.deltaMode !== 0) {1250 dX *= WHEEL_LINE_HEIGHT;1251 dY *= WHEEL_LINE_HEIGHT;1252 }12531254 // Mouse wheel events are sent in steps over VNC. This means that the VNC1255 // protocol can't handle a wheel event with specific distance or speed.1256 // Therefor, if we get a lot of small mouse wheel events we combine them.1257 this._accumulatedWheelDeltaX += dX;1258 this._accumulatedWheelDeltaY += dY;125912601261 // Generate a mouse wheel step event when the accumulated delta1262 // for one of the axes is large enough.1263 if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) {1264 if (this._accumulatedWheelDeltaX < 0) {1265 this._handleMouseButton(pos.x, pos.y, bmask | 1 << 5);1266 this._handleMouseButton(pos.x, pos.y, bmask);1267 } else if (this._accumulatedWheelDeltaX > 0) {1268 this._handleMouseButton(pos.x, pos.y, bmask | 1 << 6);1269 this._handleMouseButton(pos.x, pos.y, bmask);1270 }12711272 this._accumulatedWheelDeltaX = 0;1273 }1274 if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) {1275 if (this._accumulatedWheelDeltaY < 0) {1276 this._handleMouseButton(pos.x, pos.y, bmask | 1 << 3);1277 this._handleMouseButton(pos.x, pos.y, bmask);1278 } else if (this._accumulatedWheelDeltaY > 0) {1279 this._handleMouseButton(pos.x, pos.y, bmask | 1 << 4);1280 this._handleMouseButton(pos.x, pos.y, bmask);1281 }12821283 this._accumulatedWheelDeltaY = 0;1284 }1285 }12861287 _fakeMouseMove(ev, elementX, elementY) {1288 this._handleMouseMove(elementX, elementY);1289 this._cursor.move(ev.detail.clientX, ev.detail.clientY);1290 }12911292 _handleTapEvent(ev, bmask) {1293 let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,1294 this._canvas);12951296 // If the user quickly taps multiple times we assume they meant to1297 // hit the same spot, so slightly adjust coordinates12981299 if ((this._gestureLastTapTime !== null) &&1300 ((Date.now() - this._gestureLastTapTime) < DOUBLE_TAP_TIMEOUT) &&1301 (this._gestureFirstDoubleTapEv.detail.type === ev.detail.type)) {1302 let dx = this._gestureFirstDoubleTapEv.detail.clientX - ev.detail.clientX;1303 let dy = this._gestureFirstDoubleTapEv.detail.clientY - ev.detail.clientY;1304 let distance = Math.hypot(dx, dy);13051306 if (distance < DOUBLE_TAP_THRESHOLD) {1307 pos = clientToElement(this._gestureFirstDoubleTapEv.detail.clientX,1308 this._gestureFirstDoubleTapEv.detail.clientY,1309 this._canvas);1310 } else {1311 this._gestureFirstDoubleTapEv = ev;1312 }1313 } else {1314 this._gestureFirstDoubleTapEv = ev;1315 }1316 this._gestureLastTapTime = Date.now();13171318 this._fakeMouseMove(this._gestureFirstDoubleTapEv, pos.x, pos.y);1319 this._handleMouseButton(pos.x, pos.y, bmask);1320 this._handleMouseButton(pos.x, pos.y, 0x0);1321 }13221323 _handleGesture(ev) {1324 let magnitude;13251326 let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,1327 this._canvas);1328 switch (ev.type) {1329 case 'gesturestart':1330 switch (ev.detail.type) {1331 case 'onetap':1332 this._handleTapEvent(ev, 0x1);1333 break;1334 case 'twotap':1335 this._handleTapEvent(ev, 0x4);1336 break;1337 case 'threetap':1338 this._handleTapEvent(ev, 0x2);1339 break;1340 case 'drag':1341 if (this.dragViewport) {1342 this._viewportHasMoved = false;1343 this._viewportDragging = true;1344 this._viewportDragPos = {'x': pos.x, 'y': pos.y};1345 } else {1346 this._fakeMouseMove(ev, pos.x, pos.y);1347 this._handleMouseButton(pos.x, pos.y, 0x1);1348 }1349 break;1350 case 'longpress':1351 if (this.dragViewport) {1352 // If dragViewport is true, we need to wait to see1353 // if we have dragged outside the threshold before1354 // sending any events to the server.1355 this._viewportHasMoved = false;1356 this._viewportDragPos = {'x': pos.x, 'y': pos.y};1357 } else {1358 this._fakeMouseMove(ev, pos.x, pos.y);1359 this._handleMouseButton(pos.x, pos.y, 0x4);1360 }1361 break;1362 case 'twodrag':1363 this._gestureLastMagnitudeX = ev.detail.magnitudeX;1364 this._gestureLastMagnitudeY = ev.detail.magnitudeY;1365 this._fakeMouseMove(ev, pos.x, pos.y);1366 break;1367 case 'pinch':1368 this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX,1369 ev.detail.magnitudeY);1370 this._fakeMouseMove(ev, pos.x, pos.y);1371 break;1372 }1373 break;13741375 case 'gesturemove':1376 switch (ev.detail.type) {1377 case 'onetap':1378 case 'twotap':1379 case 'threetap':1380 break;1381 case 'drag':1382 case 'longpress':1383 if (this.dragViewport) {1384 this._viewportDragging = true;1385 const deltaX = this._viewportDragPos.x - pos.x;1386 const deltaY = this._viewportDragPos.y - pos.y;13871388 if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||1389 Math.abs(deltaY) > dragThreshold)) {1390 this._viewportHasMoved = true;13911392 this._viewportDragPos = {'x': pos.x, 'y': pos.y};1393 this._display.viewportChangePos(deltaX, deltaY);1394 }1395 } else {1396 this._fakeMouseMove(ev, pos.x, pos.y);1397 }1398 break;1399 case 'twodrag':1400 // Always scroll in the same position.1401 // We don't know if the mouse was moved so we need to move it1402 // every update.1403 this._fakeMouseMove(ev, pos.x, pos.y);1404 while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) {1405 this._handleMouseButton(pos.x, pos.y, 0x8);1406 this._handleMouseButton(pos.x, pos.y, 0x0);1407 this._gestureLastMagnitudeY += GESTURE_SCRLSENS;1408 }1409 while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) < -GESTURE_SCRLSENS) {1410 this._handleMouseButton(pos.x, pos.y, 0x10);1411 this._handleMouseButton(pos.x, pos.y, 0x0);1412 this._gestureLastMagnitudeY -= GESTURE_SCRLSENS;1413 }1414 while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) > GESTURE_SCRLSENS) {1415 this._handleMouseButton(pos.x, pos.y, 0x20);1416 this._handleMouseButton(pos.x, pos.y, 0x0);1417 this._gestureLastMagnitudeX += GESTURE_SCRLSENS;1418 }1419 while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) < -GESTURE_SCRLSENS) {1420 this._handleMouseButton(pos.x, pos.y, 0x40);1421 this._handleMouseButton(pos.x, pos.y, 0x0);1422 this._gestureLastMagnitudeX -= GESTURE_SCRLSENS;1423 }1424 break;1425 case 'pinch':1426 // Always scroll in the same position.1427 // We don't know if the mouse was moved so we need to move it1428 // every update.1429 this._fakeMouseMove(ev, pos.x, pos.y);1430 magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY);1431 if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {1432 this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);1433 while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {1434 this._handleMouseButton(pos.x, pos.y, 0x8);1435 this._handleMouseButton(pos.x, pos.y, 0x0);1436 this._gestureLastMagnitudeX += GESTURE_ZOOMSENS;1437 }1438 while ((magnitude - this._gestureLastMagnitudeX) < -GESTURE_ZOOMSENS) {1439 this._handleMouseButton(pos.x, pos.y, 0x10);1440 this._handleMouseButton(pos.x, pos.y, 0x0);1441 this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS;1442 }1443 }1444 this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", false);1445 break;1446 }1447 break;14481449 case 'gestureend':1450 switch (ev.detail.type) {1451 case 'onetap':1452 case 'twotap':1453 case 'threetap':1454 case 'pinch':1455 case 'twodrag':1456 break;1457 case 'drag':1458 if (this.dragViewport) {1459 this._viewportDragging = false;1460 } else {1461 this._fakeMouseMove(ev, pos.x, pos.y);1462 this._handleMouseButton(pos.x, pos.y, 0x0);1463 }1464 break;1465 case 'longpress':1466 if (this._viewportHasMoved) {1467 // We don't want to send any events if we have moved1468 // our viewport1469 break;1470 }14711472 if (this.dragViewport && !this._viewportHasMoved) {1473 this._fakeMouseMove(ev, pos.x, pos.y);1474 // If dragViewport is true, we need to wait to see1475 // if we have dragged outside the threshold before1476 // sending any events to the server.1477 this._handleMouseButton(pos.x, pos.y, 0x4);1478 this._handleMouseButton(pos.x, pos.y, 0x0);1479 this._viewportDragging = false;1480 } else {1481 this._fakeMouseMove(ev, pos.x, pos.y);1482 this._handleMouseButton(pos.x, pos.y, 0x0);1483 }1484 break;1485 }1486 break;1487 }1488 }14891490 _flushMouseMoveTimer(x, y) {1491 if (this._mouseMoveTimer !== null) {1492 clearTimeout(this._mouseMoveTimer);1493 this._mouseMoveTimer = null;1494 this._sendMouse(x, y, this._mouseButtonMask);1495 }1496 }14971498 // Message handlers14991500 _negotiateProtocolVersion() {1501 if (this._sock.rQwait("version", 12)) {1502 return false;1503 }15041505 const sversion = this._sock.rQshiftStr(12).substr(4, 7);1506 Log.Info("Server ProtocolVersion: " + sversion);1507 let isRepeater = 0;1508 switch (sversion) {1509 case "000.000": // UltraVNC repeater1510 isRepeater = 1;1511 break;1512 case "003.003":1513 case "003.006": // UltraVNC1514 this._rfbVersion = 3.3;1515 break;1516 case "003.007":1517 this._rfbVersion = 3.7;1518 break;1519 case "003.008":1520 case "003.889": // Apple Remote Desktop1521 case "004.000": // Intel AMT KVM1522 case "004.001": // RealVNC 4.61523 case "005.000": // RealVNC 5.31524 this._rfbVersion = 3.8;1525 break;1526 default:1527 return this._fail("Invalid server version " + sversion);1528 }15291530 if (isRepeater) {1531 let repeaterID = "ID:" + this._repeaterID;1532 while (repeaterID.length < 250) {1533 repeaterID += "\0";1534 }1535 this._sock.sQpushString(repeaterID);1536 this._sock.flush();1537 return true;1538 }15391540 if (this._rfbVersion > this._rfbMaxVersion) {1541 this._rfbVersion = this._rfbMaxVersion;1542 }15431544 const cversion = "00" + parseInt(this._rfbVersion, 10) +1545 ".00" + ((this._rfbVersion * 10) % 10);1546 this._sock.sQpushString("RFB " + cversion + "\n");1547 this._sock.flush();1548 Log.Debug('Sent ProtocolVersion: ' + cversion);15491550 this._rfbInitState = 'Security';1551 }15521553 _isSupportedSecurityType(type) {1554 const clientTypes = [1555 securityTypeNone,1556 securityTypeVNCAuth,1557 securityTypeRA2ne,1558 securityTypeTight,1559 securityTypeVeNCrypt,1560 securityTypeXVP,1561 securityTypeARD,1562 securityTypeMSLogonII,1563 securityTypePlain,1564 ];15651566 return clientTypes.includes(type);1567 }15681569 _negotiateSecurity() {1570 if (this._rfbVersion >= 3.7) {1571 // Server sends supported list, client decides1572 const numTypes = this._sock.rQshift8();1573 if (this._sock.rQwait("security type", numTypes, 1)) { return false; }15741575 if (numTypes === 0) {1576 this._rfbInitState = "SecurityReason";1577 this._securityContext = "no security types";1578 this._securityStatus = 1;1579 return true;1580 }15811582 const types = this._sock.rQshiftBytes(numTypes);1583 Log.Debug("Server security types: " + types);15841585 // Look for a matching security type in the order that the1586 // server prefers1587 this._rfbAuthScheme = -1;1588 for (let type of types) {1589 if (this._isSupportedSecurityType(type)) {1590 this._rfbAuthScheme = type;1591 break;1592 }1593 }15941595 if (this._rfbAuthScheme === -1) {1596 return this._fail("Unsupported security types (types: " + types + ")");1597 }15981599 this._sock.sQpush8(this._rfbAuthScheme);1600 this._sock.flush();1601 } else {1602 // Server decides1603 if (this._sock.rQwait("security scheme", 4)) { return false; }1604 this._rfbAuthScheme = this._sock.rQshift32();16051606 if (this._rfbAuthScheme == 0) {1607 this._rfbInitState = "SecurityReason";1608 this._securityContext = "authentication scheme";1609 this._securityStatus = 1;1610 return true;1611 }1612 }16131614 this._rfbInitState = 'Authentication';1615 Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);16161617 return true;1618 }16191620 _handleSecurityReason() {1621 if (this._sock.rQwait("reason length", 4)) {1622 return false;1623 }1624 const strlen = this._sock.rQshift32();1625 let reason = "";16261627 if (strlen > 0) {1628 if (this._sock.rQwait("reason", strlen, 4)) { return false; }1629 reason = this._sock.rQshiftStr(strlen);1630 }16311632 if (reason !== "") {1633 this.dispatchEvent(new CustomEvent(1634 "securityfailure",1635 { detail: { status: this._securityStatus,1636 reason: reason } }));16371638 return this._fail("Security negotiation failed on " +1639 this._securityContext +1640 " (reason: " + reason + ")");1641 } else {1642 this.dispatchEvent(new CustomEvent(1643 "securityfailure",1644 { detail: { status: this._securityStatus } }));16451646 return this._fail("Security negotiation failed on " +1647 this._securityContext);1648 }1649 }16501651 // authentication1652 _negotiateXvpAuth() {1653 if (this._rfbCredentials.username === undefined ||1654 this._rfbCredentials.password === undefined ||1655 this._rfbCredentials.target === undefined) {1656 this.dispatchEvent(new CustomEvent(1657 "credentialsrequired",1658 { detail: { types: ["username", "password", "target"] } }));1659 return false;1660 }16611662 this._sock.sQpush8(this._rfbCredentials.username.length);1663 this._sock.sQpush8(this._rfbCredentials.target.length);1664 this._sock.sQpushString(this._rfbCredentials.username);1665 this._sock.sQpushString(this._rfbCredentials.target);16661667 this._sock.flush();16681669 this._rfbAuthScheme = securityTypeVNCAuth;16701671 return this._negotiateAuthentication();1672 }16731674 // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype1675 _negotiateVeNCryptAuth() {16761677 // waiting for VeNCrypt version1678 if (this._rfbVeNCryptState == 0) {1679 if (this._sock.rQwait("vencrypt version", 2)) { return false; }16801681 const major = this._sock.rQshift8();1682 const minor = this._sock.rQshift8();16831684 if (!(major == 0 && minor == 2)) {1685 return this._fail("Unsupported VeNCrypt version " + major + "." + minor);1686 }16871688 this._sock.sQpush8(0);1689 this._sock.sQpush8(2);1690 this._sock.flush();1691 this._rfbVeNCryptState = 1;1692 }16931694 // waiting for ACK1695 if (this._rfbVeNCryptState == 1) {1696 if (this._sock.rQwait("vencrypt ack", 1)) { return false; }16971698 const res = this._sock.rQshift8();16991700 if (res != 0) {1701 return this._fail("VeNCrypt failure " + res);1702 }17031704 this._rfbVeNCryptState = 2;1705 }1706 // must fall through here (i.e. no "else if"), beacause we may have already received1707 // the subtypes length and won't be called again17081709 if (this._rfbVeNCryptState == 2) { // waiting for subtypes length1710 if (this._sock.rQwait("vencrypt subtypes length", 1)) { return false; }17111712 const subtypesLength = this._sock.rQshift8();1713 if (subtypesLength < 1) {1714 return this._fail("VeNCrypt subtypes empty");1715 }17161717 this._rfbVeNCryptSubtypesLength = subtypesLength;1718 this._rfbVeNCryptState = 3;1719 }17201721 // waiting for subtypes list1722 if (this._rfbVeNCryptState == 3) {1723 if (this._sock.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength)) { return false; }17241725 const subtypes = [];1726 for (let i = 0; i < this._rfbVeNCryptSubtypesLength; i++) {1727 subtypes.push(this._sock.rQshift32());1728 }17291730 // Look for a matching security type in the order that the1731 // server prefers1732 this._rfbAuthScheme = -1;1733 for (let type of subtypes) {1734 // Avoid getting in to a loop1735 if (type === securityTypeVeNCrypt) {1736 continue;1737 }17381739 if (this._isSupportedSecurityType(type)) {1740 this._rfbAuthScheme = type;1741 break;1742 }1743 }17441745 if (this._rfbAuthScheme === -1) {1746 return this._fail("Unsupported security types (types: " + subtypes + ")");1747 }17481749 this._sock.sQpush32(this._rfbAuthScheme);1750 this._sock.flush();17511752 this._rfbVeNCryptState = 4;1753 return true;1754 }1755 }17561757 _negotiatePlainAuth() {1758 if (this._rfbCredentials.username === undefined ||1759 this._rfbCredentials.password === undefined) {1760 this.dispatchEvent(new CustomEvent(1761 "credentialsrequired",1762 { detail: { types: ["username", "password"] } }));1763 return false;1764 }17651766 const user = encodeUTF8(this._rfbCredentials.username);1767 const pass = encodeUTF8(this._rfbCredentials.password);17681769 this._sock.sQpush32(user.length);1770 this._sock.sQpush32(pass.length);1771 this._sock.sQpushString(user);1772 this._sock.sQpushString(pass);1773 this._sock.flush();17741775 this._rfbInitState = "SecurityResult";1776 return true;1777 }17781779 _negotiateStdVNCAuth() {1780 if (this._sock.rQwait("auth challenge", 16)) { return false; }17811782 if (this._rfbCredentials.password === undefined) {1783 this.dispatchEvent(new CustomEvent(1784 "credentialsrequired",1785 { detail: { types: ["password"] } }));1786 return false;1787 }17881789 // TODO(directxman12): make genDES not require an Array1790 const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));1791 const response = RFB.genDES(this._rfbCredentials.password, challenge);1792 this._sock.sQpushBytes(response);1793 this._sock.flush();1794 this._rfbInitState = "SecurityResult";1795 return true;1796 }17971798 _negotiateARDAuth() {17991800 if (this._rfbCredentials.username === undefined ||1801 this._rfbCredentials.password === undefined) {1802 this.dispatchEvent(new CustomEvent(1803 "credentialsrequired",1804 { detail: { types: ["username", "password"] } }));1805 return false;1806 }18071808 if (this._rfbCredentials.ardPublicKey != undefined &&1809 this._rfbCredentials.ardCredentials != undefined) {1810 // if the async web crypto is done return the results1811 this._sock.sQpushBytes(this._rfbCredentials.ardCredentials);1812 this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey);1813 this._sock.flush();1814 this._rfbCredentials.ardCredentials = null;1815 this._rfbCredentials.ardPublicKey = null;1816 this._rfbInitState = "SecurityResult";1817 return true;1818 }18191820 if (this._sock.rQwait("read ard", 4)) { return false; }18211822 let generator = this._sock.rQshiftBytes(2); // DH base generator value18231824 let keyLength = this._sock.rQshift16();18251826 if (this._sock.rQwait("read ard keylength", keyLength*2, 4)) { return false; }18271828 // read the server values1829 let prime = this._sock.rQshiftBytes(keyLength); // predetermined prime modulus1830 let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key18311832 let clientKey = legacyCrypto.generateKey(1833 { name: "DH", g: generator, p: prime }, false, ["deriveBits"]);1834 this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);18351836 return false;1837 }18381839 async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {1840 const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);1841 const sharedKey = legacyCrypto.deriveBits(1842 { name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);18431844 const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);1845 const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);18461847 const credentials = window.crypto.getRandomValues(new Uint8Array(128));1848 for (let i = 0; i < username.length; i++) {1849 credentials[i] = username.charCodeAt(i);1850 }1851 credentials[username.length] = 0;1852 for (let i = 0; i < password.length; i++) {1853 credentials[64 + i] = password.charCodeAt(i);1854 }1855 credentials[64 + password.length] = 0;18561857 const key = await legacyCrypto.digest("MD5", sharedKey);1858 const cipher = await legacyCrypto.importKey(1859 "raw", key, { name: "AES-ECB" }, false, ["encrypt"]);1860 const encrypted = await legacyCrypto.encrypt({ name: "AES-ECB" }, cipher, credentials);18611862 this._rfbCredentials.ardCredentials = encrypted;1863 this._rfbCredentials.ardPublicKey = clientPublicKey;18641865 this._resumeAuthentication();1866 }18671868 _negotiateTightUnixAuth() {1869 if (this._rfbCredentials.username === undefined ||1870 this._rfbCredentials.password === undefined) {1871 this.dispatchEvent(new CustomEvent(1872 "credentialsrequired",1873 { detail: { types: ["username", "password"] } }));1874 return false;1875 }18761877 this._sock.sQpush32(this._rfbCredentials.username.length);1878 this._sock.sQpush32(this._rfbCredentials.password.length);1879 this._sock.sQpushString(this._rfbCredentials.username);1880 this._sock.sQpushString(this._rfbCredentials.password);1881 this._sock.flush();18821883 this._rfbInitState = "SecurityResult";1884 return true;1885 }18861887 _negotiateTightTunnels(numTunnels) {1888 const clientSupportedTunnelTypes = {1889 0: { vendor: 'TGHT', signature: 'NOTUNNEL' }1890 };1891 const serverSupportedTunnelTypes = {};1892 // receive tunnel capabilities1893 for (let i = 0; i < numTunnels; i++) {1894 const capCode = this._sock.rQshift32();1895 const capVendor = this._sock.rQshiftStr(4);1896 const capSignature = this._sock.rQshiftStr(8);1897 serverSupportedTunnelTypes[capCode] = { vendor: capVendor, signature: capSignature };1898 }18991900 Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes);19011902 // Siemens touch panels have a VNC server that supports NOTUNNEL,1903 // but forgets to advertise it. Try to detect such servers by1904 // looking for their custom tunnel type.1905 if (serverSupportedTunnelTypes[1] &&1906 (serverSupportedTunnelTypes[1].vendor === "SICR") &&1907 (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) {1908 Log.Debug("Detected Siemens server. Assuming NOTUNNEL support.");1909 serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };1910 }19111912 // choose the notunnel type1913 if (serverSupportedTunnelTypes[0]) {1914 if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||1915 serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {1916 return this._fail("Client's tunnel type had the incorrect " +1917 "vendor or signature");1918 }1919 Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);1920 this._sock.sQpush32(0); // use NOTUNNEL1921 this._sock.flush();1922 return false; // wait until we receive the sub auth count to continue1923 } else {1924 return this._fail("Server wanted tunnels, but doesn't support " +1925 "the notunnel type");1926 }1927 }19281929 _negotiateTightAuth() {1930 if (!this._rfbTightVNC) { // first pass, do the tunnel negotiation1931 if (this._sock.rQwait("num tunnels", 4)) { return false; }1932 const numTunnels = this._sock.rQshift32();1933 if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }19341935 this._rfbTightVNC = true;19361937 if (numTunnels > 0) {1938 this._negotiateTightTunnels(numTunnels);1939 return false; // wait until we receive the sub auth to continue1940 }1941 }19421943 // second pass, do the sub-auth negotiation1944 if (this._sock.rQwait("sub auth count", 4)) { return false; }1945 const subAuthCount = this._sock.rQshift32();1946 if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected1947 this._rfbInitState = 'SecurityResult';1948 return true;1949 }19501951 if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }19521953 const clientSupportedTypes = {1954 'STDVNOAUTH__': 1,1955 'STDVVNCAUTH_': 2,1956 'TGHTULGNAUTH': 1291957 };19581959 const serverSupportedTypes = [];19601961 for (let i = 0; i < subAuthCount; i++) {1962 this._sock.rQshift32(); // capNum1963 const capabilities = this._sock.rQshiftStr(12);1964 serverSupportedTypes.push(capabilities);1965 }19661967 Log.Debug("Server Tight authentication types: " + serverSupportedTypes);19681969 for (let authType in clientSupportedTypes) {1970 if (serverSupportedTypes.indexOf(authType) != -1) {1971 this._sock.sQpush32(clientSupportedTypes[authType]);1972 this._sock.flush();1973 Log.Debug("Selected authentication type: " + authType);19741975 switch (authType) {1976 case 'STDVNOAUTH__': // no auth1977 this._rfbInitState = 'SecurityResult';1978 return true;1979 case 'STDVVNCAUTH_':1980 this._rfbAuthScheme = securityTypeVNCAuth;1981 return true;1982 case 'TGHTULGNAUTH':1983 this._rfbAuthScheme = securityTypeUnixLogon;1984 return true;1985 default:1986 return this._fail("Unsupported tiny auth scheme " +1987 "(scheme: " + authType + ")");1988 }1989 }1990 }19911992 return this._fail("No supported sub-auth types!");1993 }19941995 _handleRSAAESCredentialsRequired(event) {1996 this.dispatchEvent(event);1997 }19981999 _handleRSAAESServerVerification(event) {2000 this.dispatchEvent(event);2001 }20022003 _negotiateRA2neAuth() {2004 if (this._rfbRSAAESAuthenticationState === null) {2005 this._rfbRSAAESAuthenticationState = new RSAAESAuthenticationState(this._sock, () => this._rfbCredentials);2006 this._rfbRSAAESAuthenticationState.addEventListener(2007 "serververification", this._eventHandlers.handleRSAAESServerVerification);2008 this._rfbRSAAESAuthenticationState.addEventListener(2009 "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);2010 }2011 this._rfbRSAAESAuthenticationState.checkInternalEvents();2012 if (!this._rfbRSAAESAuthenticationState.hasStarted) {2013 this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync()2014 .catch((e) => {2015 if (e.message !== "disconnect normally") {2016 this._fail(e.message);2017 }2018 })2019 .then(() => {2020 this._rfbInitState = "SecurityResult";2021 return true;2022 }).finally(() => {2023 this._rfbRSAAESAuthenticationState.removeEventListener(2024 "serververification", this._eventHandlers.handleRSAAESServerVerification);2025 this._rfbRSAAESAuthenticationState.removeEventListener(2026 "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);2027 this._rfbRSAAESAuthenticationState = null;2028 });2029 }2030 return false;2031 }20322033 _negotiateMSLogonIIAuth() {2034 if (this._sock.rQwait("mslogonii dh param", 24)) { return false; }20352036 if (this._rfbCredentials.username === undefined ||2037 this._rfbCredentials.password === undefined) {2038 this.dispatchEvent(new CustomEvent(2039 "credentialsrequired",2040 { detail: { types: ["username", "password"] } }));2041 return false;2042 }20432044 const g = this._sock.rQshiftBytes(8);2045 const p = this._sock.rQshiftBytes(8);2046 const A = this._sock.rQshiftBytes(8);2047 const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]);2048 const B = legacyCrypto.exportKey("raw", dhKey.publicKey);2049 const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64);20502051 const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]);2052 const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);2053 const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);2054 let usernameBytes = new Uint8Array(256);2055 let passwordBytes = new Uint8Array(64);2056 window.crypto.getRandomValues(usernameBytes);2057 window.crypto.getRandomValues(passwordBytes);2058 for (let i = 0; i < username.length; i++) {2059 usernameBytes[i] = username.charCodeAt(i);2060 }2061 usernameBytes[username.length] = 0;2062 for (let i = 0; i < password.length; i++) {2063 passwordBytes[i] = password.charCodeAt(i);2064 }2065 passwordBytes[password.length] = 0;2066 usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes);2067 passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes);2068 this._sock.sQpushBytes(B);2069 this._sock.sQpushBytes(usernameBytes);2070 this._sock.sQpushBytes(passwordBytes);2071 this._sock.flush();2072 this._rfbInitState = "SecurityResult";2073 return true;2074 }20752076 _negotiateAuthentication() {2077 switch (this._rfbAuthScheme) {2078 case securityTypeNone:2079 if (this._rfbVersion >= 3.8) {2080 this._rfbInitState = 'SecurityResult';2081 } else {2082 this._rfbInitState = 'ClientInitialisation';2083 }2084 return true;20852086 case securityTypeXVP:2087 return this._negotiateXvpAuth();20882089 case securityTypeARD:2090 return this._negotiateARDAuth();20912092 case securityTypeVNCAuth:2093 return this._negotiateStdVNCAuth();20942095 case securityTypeTight:2096 return this._negotiateTightAuth();20972098 case securityTypeVeNCrypt:2099 return this._negotiateVeNCryptAuth();21002101 case securityTypePlain:2102 return this._negotiatePlainAuth();21032104 case securityTypeUnixLogon:2105 return this._negotiateTightUnixAuth();21062107 case securityTypeRA2ne:2108 return this._negotiateRA2neAuth();21092110 case securityTypeMSLogonII:2111 return this._negotiateMSLogonIIAuth();21122113 default:2114 return this._fail("Unsupported auth scheme (scheme: " +2115 this._rfbAuthScheme + ")");2116 }2117 }21182119 _handleSecurityResult() {2120 if (this._sock.rQwait('VNC auth response ', 4)) { return false; }21212122 const status = this._sock.rQshift32();21232124 if (status === 0) { // OK2125 this._rfbInitState = 'ClientInitialisation';2126 Log.Debug('Authentication OK');2127 return true;2128 } else {2129 if (this._rfbVersion >= 3.8) {2130 this._rfbInitState = "SecurityReason";2131 this._securityContext = "security result";2132 this._securityStatus = status;2133 return true;2134 } else {2135 this.dispatchEvent(new CustomEvent(2136 "securityfailure",2137 { detail: { status: status } }));21382139 return this._fail("Security handshake failed");2140 }2141 }2142 }21432144 _negotiateServerInit() {2145 if (this._sock.rQwait("server initialization", 24)) { return false; }21462147 /* Screen size */2148 const width = this._sock.rQshift16();2149 const height = this._sock.rQshift16();21502151 /* PIXEL_FORMAT */2152 const bpp = this._sock.rQshift8();2153 const depth = this._sock.rQshift8();2154 const bigEndian = this._sock.rQshift8();2155 const trueColor = this._sock.rQshift8();21562157 const redMax = this._sock.rQshift16();2158 const greenMax = this._sock.rQshift16();2159 const blueMax = this._sock.rQshift16();2160 const redShift = this._sock.rQshift8();2161 const greenShift = this._sock.rQshift8();2162 const blueShift = this._sock.rQshift8();2163 this._sock.rQskipBytes(3); // padding21642165 // NB(directxman12): we don't want to call any callbacks or print messages until2166 // *after* we're past the point where we could backtrack21672168 /* Connection name/title */2169 const nameLength = this._sock.rQshift32();2170 if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }2171 let name = this._sock.rQshiftStr(nameLength);2172 name = decodeUTF8(name, true);21732174 if (this._rfbTightVNC) {2175 if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }2176 // In TightVNC mode, ServerInit message is extended2177 const numServerMessages = this._sock.rQshift16();2178 const numClientMessages = this._sock.rQshift16();2179 const numEncodings = this._sock.rQshift16();2180 this._sock.rQskipBytes(2); // padding21812182 const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;2183 if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }21842185 // we don't actually do anything with the capability information that TIGHT sends,2186 // so we just skip the all of this.21872188 // TIGHT server message capabilities2189 this._sock.rQskipBytes(16 * numServerMessages);21902191 // TIGHT client message capabilities2192 this._sock.rQskipBytes(16 * numClientMessages);21932194 // TIGHT encoding capabilities2195 this._sock.rQskipBytes(16 * numEncodings);2196 }21972198 // NB(directxman12): these are down here so that we don't run them multiple times2199 // if we backtrack2200 Log.Info("Screen: " + width + "x" + height +2201 ", bpp: " + bpp + ", depth: " + depth +2202 ", bigEndian: " + bigEndian +2203 ", trueColor: " + trueColor +2204 ", redMax: " + redMax +2205 ", greenMax: " + greenMax +2206 ", blueMax: " + blueMax +2207 ", redShift: " + redShift +2208 ", greenShift: " + greenShift +2209 ", blueShift: " + blueShift);22102211 // we're past the point where we could backtrack, so it's safe to call this2212 this._setDesktopName(name);2213 this._resize(width, height);22142215 if (!this._viewOnly) { this._keyboard.grab(); }22162217 this._fbDepth = 24;22182219 if (this._fbName === "Intel(r) AMT KVM") {2220 Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");2221 this._fbDepth = 8;2222 }22232224 RFB.messages.pixelFormat(this._sock, this._fbDepth, true);2225 this._sendEncodings();2226 RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);22272228 this._updateConnectionState('connected');2229 return true;2230 }22312232 _sendEncodings() {2233 const encs = [];22342235 // In preference order2236 encs.push(encodings.encodingCopyRect);2237 // Only supported with full depth support2238 if (this._fbDepth == 24) {2239 if (supportsWebCodecsH264Decode) {2240 encs.push(encodings.encodingH264);2241 }2242 encs.push(encodings.encodingTight);2243 encs.push(encodings.encodingTightPNG);2244 encs.push(encodings.encodingZRLE);2245 encs.push(encodings.encodingJPEG);2246 encs.push(encodings.encodingHextile);2247 encs.push(encodings.encodingRRE);2248 encs.push(encodings.encodingZlib);2249 }2250 encs.push(encodings.encodingRaw);22512252 // Psuedo-encoding settings2253 encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);2254 encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);22552256 encs.push(encodings.pseudoEncodingDesktopSize);2257 encs.push(encodings.pseudoEncodingLastRect);2258 encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);2259 encs.push(encodings.pseudoEncodingQEMULedEvent);2260 encs.push(encodings.pseudoEncodingExtendedDesktopSize);2261 encs.push(encodings.pseudoEncodingXvp);2262 encs.push(encodings.pseudoEncodingFence);2263 encs.push(encodings.pseudoEncodingContinuousUpdates);2264 encs.push(encodings.pseudoEncodingDesktopName);2265 encs.push(encodings.pseudoEncodingExtendedClipboard);2266 encs.push(encodings.pseudoEncodingExtendedMouseButtons);22672268 if (this._fbDepth == 24) {2269 encs.push(encodings.pseudoEncodingVMwareCursor);2270 encs.push(encodings.pseudoEncodingCursor);2271 }22722273 RFB.messages.clientEncodings(this._sock, encs);2274 }22752276 /* RFB protocol initialization states:2277 * ProtocolVersion2278 * Security2279 * Authentication2280 * SecurityResult2281 * ClientInitialization - not triggered by server message2282 * ServerInitialization2283 */2284 _initMsg() {2285 switch (this._rfbInitState) {2286 case 'ProtocolVersion':2287 return this._negotiateProtocolVersion();22882289 case 'Security':2290 return this._negotiateSecurity();22912292 case 'Authentication':2293 return this._negotiateAuthentication();22942295 case 'SecurityResult':2296 return this._handleSecurityResult();22972298 case 'SecurityReason':2299 return this._handleSecurityReason();23002301 case 'ClientInitialisation':2302 this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation2303 this._sock.flush();2304 this._rfbInitState = 'ServerInitialisation';2305 return true;23062307 case 'ServerInitialisation':2308 return this._negotiateServerInit();23092310 default:2311 return this._fail("Unknown init state (state: " +2312 this._rfbInitState + ")");2313 }2314 }23152316 // Resume authentication handshake after it was paused for some2317 // reason, e.g. waiting for a password from the user2318 _resumeAuthentication() {2319 // We use setTimeout() so it's run in its own context, just like2320 // it originally did via the WebSocket's event handler2321 setTimeout(this._initMsg.bind(this), 0);2322 }23232324 _handleSetColourMapMsg() {2325 Log.Debug("SetColorMapEntries");23262327 return this._fail("Unexpected SetColorMapEntries message");2328 }23292330 _handleServerCutText() {2331 Log.Debug("ServerCutText");23322333 if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }23342335 this._sock.rQskipBytes(3); // Padding23362337 let length = this._sock.rQshift32();2338 length = toSigned32bit(length);23392340 if (this._sock.rQwait("ServerCutText content", Math.abs(length), 8)) { return false; }23412342 if (length >= 0) {2343 //Standard msg2344 const text = this._sock.rQshiftStr(length);2345 if (this._viewOnly) {2346 return true;2347 }23482349 this.dispatchEvent(new CustomEvent(2350 "clipboard",2351 { detail: { text: text } }));23522353 } else {2354 //Extended msg.2355 length = Math.abs(length);2356 const flags = this._sock.rQshift32();2357 let formats = flags & 0x0000FFFF;2358 let actions = flags & 0xFF000000;23592360 let isCaps = (!!(actions & extendedClipboardActionCaps));2361 if (isCaps) {2362 this._clipboardServerCapabilitiesFormats = {};2363 this._clipboardServerCapabilitiesActions = {};23642365 // Update our server capabilities for Formats2366 for (let i = 0; i <= 15; i++) {2367 let index = 1 << i;23682369 // Check if format flag is set.2370 if ((formats & index)) {2371 this._clipboardServerCapabilitiesFormats[index] = true;2372 // We don't send unsolicited clipboard, so we2373 // ignore the size2374 this._sock.rQshift32();2375 }2376 }23772378 // Update our server capabilities for Actions2379 for (let i = 24; i <= 31; i++) {2380 let index = 1 << i;2381 this._clipboardServerCapabilitiesActions[index] = !!(actions & index);2382 }23832384 /* Caps handling done, send caps with the clients2385 capabilities set as a response */2386 let clientActions = [2387 extendedClipboardActionCaps,2388 extendedClipboardActionRequest,2389 extendedClipboardActionPeek,2390 extendedClipboardActionNotify,2391 extendedClipboardActionProvide2392 ];2393 RFB.messages.extendedClipboardCaps(this._sock, clientActions, {extendedClipboardFormatText: 0});23942395 } else if (actions === extendedClipboardActionRequest) {2396 if (this._viewOnly) {2397 return true;2398 }23992400 // Check if server has told us it can handle Provide and there is clipboard data to send.2401 if (this._clipboardText != null &&2402 this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) {24032404 if (formats & extendedClipboardFormatText) {2405 RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]);2406 }2407 }24082409 } else if (actions === extendedClipboardActionPeek) {2410 if (this._viewOnly) {2411 return true;2412 }24132414 if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {24152416 if (this._clipboardText != null) {2417 RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);2418 } else {2419 RFB.messages.extendedClipboardNotify(this._sock, []);2420 }2421 }24222423 } else if (actions === extendedClipboardActionNotify) {2424 if (this._viewOnly) {2425 return true;2426 }24272428 if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) {24292430 if (formats & extendedClipboardFormatText) {2431 RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]);2432 }2433 }24342435 } else if (actions === extendedClipboardActionProvide) {2436 if (this._viewOnly) {2437 return true;2438 }24392440 if (!(formats & extendedClipboardFormatText)) {2441 return true;2442 }2443 // Ignore what we had in our clipboard client side.2444 this._clipboardText = null;24452446 // FIXME: Should probably verify that this data was actually requested2447 let zlibStream = this._sock.rQshiftBytes(length - 4);2448 let streamInflator = new Inflator();2449 let textData = null;24502451 streamInflator.setInput(zlibStream);2452 for (let i = 0; i <= 15; i++) {2453 let format = 1 << i;24542455 if (formats & format) {24562457 let size = 0x00;2458 let sizeArray = streamInflator.inflate(4);24592460 size |= (sizeArray[0] << 24);2461 size |= (sizeArray[1] << 16);2462 size |= (sizeArray[2] << 8);2463 size |= (sizeArray[3]);2464 let chunk = streamInflator.inflate(size);24652466 if (format === extendedClipboardFormatText) {2467 textData = chunk;2468 }2469 }2470 }2471 streamInflator.setInput(null);24722473 if (textData !== null) {2474 let tmpText = "";2475 for (let i = 0; i < textData.length; i++) {2476 tmpText += String.fromCharCode(textData[i]);2477 }2478 textData = tmpText;24792480 textData = decodeUTF8(textData);2481 if ((textData.length > 0) && "\0" === textData.charAt(textData.length - 1)) {2482 textData = textData.slice(0, -1);2483 }24842485 textData = textData.replaceAll("\r\n", "\n");24862487 this.dispatchEvent(new CustomEvent(2488 "clipboard",2489 { detail: { text: textData } }));2490 }2491 } else {2492 return this._fail("Unexpected action in extended clipboard message: " + actions);2493 }2494 }2495 return true;2496 }24972498 _handleServerFenceMsg() {2499 if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; }2500 this._sock.rQskipBytes(3); // Padding2501 let flags = this._sock.rQshift32();2502 let length = this._sock.rQshift8();25032504 if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; }25052506 if (length > 64) {2507 Log.Warn("Bad payload length (" + length + ") in fence response");2508 length = 64;2509 }25102511 const payload = this._sock.rQshiftStr(length);25122513 this._supportsFence = true;25142515 /*2516 * Fence flags2517 *2518 * (1<<0) - BlockBefore2519 * (1<<1) - BlockAfter2520 * (1<<2) - SyncNext2521 * (1<<31) - Request2522 */25232524 if (!(flags & (1<<31))) {2525 return this._fail("Unexpected fence response");2526 }25272528 // Filter out unsupported flags2529 // FIXME: support syncNext2530 flags &= (1<<0) | (1<<1);25312532 // BlockBefore and BlockAfter are automatically handled by2533 // the fact that we process each incoming message2534 // synchronuosly.2535 RFB.messages.clientFence(this._sock, flags, payload);25362537 return true;2538 }25392540 _handleXvpMsg() {2541 if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }2542 this._sock.rQskipBytes(1); // Padding2543 const xvpVer = this._sock.rQshift8();2544 const xvpMsg = this._sock.rQshift8();25452546 switch (xvpMsg) {2547 case 0: // XVP_FAIL2548 Log.Error("XVP operation failed");2549 break;2550 case 1: // XVP_INIT2551 this._rfbXvpVer = xvpVer;2552 Log.Info("XVP extensions enabled (version " + this._rfbXvpVer + ")");2553 this._setCapability("power", true);2554 break;2555 default:2556 this._fail("Illegal server XVP message (msg: " + xvpMsg + ")");2557 break;2558 }25592560 return true;2561 }25622563 _normalMsg() {2564 let msgType;2565 if (this._FBU.rects > 0) {2566 msgType = 0;2567 } else {2568 msgType = this._sock.rQshift8();2569 }25702571 let first, ret;2572 switch (msgType) {2573 case 0: // FramebufferUpdate2574 ret = this._framebufferUpdate();2575 if (ret && !this._enabledContinuousUpdates) {2576 RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,2577 this._fbWidth, this._fbHeight);2578 }2579 return ret;25802581 case 1: // SetColorMapEntries2582 return this._handleSetColourMapMsg();25832584 case 2: // Bell2585 Log.Debug("Bell");2586 this.dispatchEvent(new CustomEvent(2587 "bell",2588 { detail: {} }));2589 return true;25902591 case 3: // ServerCutText2592 return this._handleServerCutText();25932594 case 150: // EndOfContinuousUpdates2595 first = !this._supportsContinuousUpdates;2596 this._supportsContinuousUpdates = true;2597 this._enabledContinuousUpdates = false;2598 if (first) {2599 this._enabledContinuousUpdates = true;2600 this._updateContinuousUpdates();2601 Log.Info("Enabling continuous updates.");2602 } else {2603 // FIXME: We need to send a framebufferupdaterequest here2604 // if we add support for turning off continuous updates2605 }2606 return true;26072608 case 248: // ServerFence2609 return this._handleServerFenceMsg();26102611 case 250: // XVP2612 return this._handleXvpMsg();26132614 default:2615 this._fail("Unexpected server message (type " + msgType + ")");2616 Log.Debug("sock.rQpeekBytes(30): " + this._sock.rQpeekBytes(30));2617 return true;2618 }2619 }26202621 _framebufferUpdate() {2622 if (this._FBU.rects === 0) {2623 if (this._sock.rQwait("FBU header", 3, 1)) { return false; }2624 this._sock.rQskipBytes(1); // Padding2625 this._FBU.rects = this._sock.rQshift16();26262627 // Make sure the previous frame is fully rendered first2628 // to avoid building up an excessive queue2629 if (this._display.pending()) {2630 this._flushing = true;2631 this._display.flush()2632 .then(() => {2633 this._flushing = false;2634 // Resume processing2635 if (!this._sock.rQwait("message", 1)) {2636 this._handleMessage();2637 }2638 });2639 return false;2640 }2641 }26422643 while (this._FBU.rects > 0) {2644 if (this._FBU.encoding === null) {2645 if (this._sock.rQwait("rect header", 12)) { return false; }2646 /* New FramebufferUpdate */26472648 this._FBU.x = this._sock.rQshift16();2649 this._FBU.y = this._sock.rQshift16();2650 this._FBU.width = this._sock.rQshift16();2651 this._FBU.height = this._sock.rQshift16();2652 this._FBU.encoding = this._sock.rQshift32();2653 /* Encodings are signed */2654 this._FBU.encoding >>= 0;2655 }26562657 if (!this._handleRect()) {2658 return false;2659 }26602661 this._FBU.rects--;2662 this._FBU.encoding = null;2663 }26642665 this._display.flip();26662667 return true; // We finished this FBU2668 }26692670 _handleRect() {2671 switch (this._FBU.encoding) {2672 case encodings.pseudoEncodingLastRect:2673 this._FBU.rects = 1; // Will be decreased when we return2674 return true;26752676 case encodings.pseudoEncodingVMwareCursor:2677 return this._handleVMwareCursor();26782679 case encodings.pseudoEncodingCursor:2680 return this._handleCursor();26812682 case encodings.pseudoEncodingQEMUExtendedKeyEvent:2683 this._qemuExtKeyEventSupported = true;2684 return true;26852686 case encodings.pseudoEncodingDesktopName:2687 return this._handleDesktopName();26882689 case encodings.pseudoEncodingDesktopSize:2690 this._resize(this._FBU.width, this._FBU.height);2691 return true;26922693 case encodings.pseudoEncodingExtendedDesktopSize:2694 return this._handleExtendedDesktopSize();26952696 case encodings.pseudoEncodingExtendedMouseButtons:2697 this._extendedPointerEventSupported = true;2698 return true;26992700 case encodings.pseudoEncodingQEMULedEvent:2701 return this._handleLedEvent();27022703 default:2704 return this._handleDataRect();2705 }2706 }27072708 _handleVMwareCursor() {2709 const hotx = this._FBU.x; // hotspot-x2710 const hoty = this._FBU.y; // hotspot-y2711 const w = this._FBU.width;2712 const h = this._FBU.height;2713 if (this._sock.rQwait("VMware cursor encoding", 1)) {2714 return false;2715 }27162717 const cursorType = this._sock.rQshift8();27182719 this._sock.rQshift8(); //Padding27202721 let rgba;2722 const bytesPerPixel = 4;27232724 //Classic cursor2725 if (cursorType == 0) {2726 //Used to filter away unimportant bits.2727 //OR is used for correct conversion in js.2728 const PIXEL_MASK = 0xffffff00 | 0;2729 rgba = new Array(w * h * bytesPerPixel);27302731 if (this._sock.rQwait("VMware cursor classic encoding",2732 (w * h * bytesPerPixel) * 2, 2)) {2733 return false;2734 }27352736 let andMask = new Array(w * h);2737 for (let pixel = 0; pixel < (w * h); pixel++) {2738 andMask[pixel] = this._sock.rQshift32();2739 }27402741 let xorMask = new Array(w * h);2742 for (let pixel = 0; pixel < (w * h); pixel++) {2743 xorMask[pixel] = this._sock.rQshift32();2744 }27452746 for (let pixel = 0; pixel < (w * h); pixel++) {2747 if (andMask[pixel] == 0) {2748 //Fully opaque pixel2749 let bgr = xorMask[pixel];2750 let r = bgr >> 8 & 0xff;2751 let g = bgr >> 16 & 0xff;2752 let b = bgr >> 24 & 0xff;27532754 rgba[(pixel * bytesPerPixel) ] = r; //r2755 rgba[(pixel * bytesPerPixel) + 1 ] = g; //g2756 rgba[(pixel * bytesPerPixel) + 2 ] = b; //b2757 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff; //a27582759 } else if ((andMask[pixel] & PIXEL_MASK) ==2760 PIXEL_MASK) {2761 //Only screen value matters, no mouse colouring2762 if (xorMask[pixel] == 0) {2763 //Transparent pixel2764 rgba[(pixel * bytesPerPixel) ] = 0x00;2765 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;2766 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;2767 rgba[(pixel * bytesPerPixel) + 3 ] = 0x00;27682769 } else if ((xorMask[pixel] & PIXEL_MASK) ==2770 PIXEL_MASK) {2771 //Inverted pixel, not supported in browsers.2772 //Fully opaque instead.2773 rgba[(pixel * bytesPerPixel) ] = 0x00;2774 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;2775 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;2776 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;27772778 } else {2779 //Unhandled xorMask2780 rgba[(pixel * bytesPerPixel) ] = 0x00;2781 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;2782 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;2783 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;2784 }27852786 } else {2787 //Unhandled andMask2788 rgba[(pixel * bytesPerPixel) ] = 0x00;2789 rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;2790 rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;2791 rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;2792 }2793 }27942795 //Alpha cursor.2796 } else if (cursorType == 1) {2797 if (this._sock.rQwait("VMware cursor alpha encoding",2798 (w * h * 4), 2)) {2799 return false;2800 }28012802 rgba = new Array(w * h * bytesPerPixel);28032804 for (let pixel = 0; pixel < (w * h); pixel++) {2805 let data = this._sock.rQshift32();28062807 rgba[(pixel * 4) ] = data >> 24 & 0xff; //r2808 rgba[(pixel * 4) + 1 ] = data >> 16 & 0xff; //g2809 rgba[(pixel * 4) + 2 ] = data >> 8 & 0xff; //b2810 rgba[(pixel * 4) + 3 ] = data & 0xff; //a2811 }28122813 } else {2814 Log.Warn("The given cursor type is not supported: "2815 + cursorType + " given.");2816 return false;2817 }28182819 this._updateCursor(rgba, hotx, hoty, w, h);28202821 return true;2822 }28232824 _handleCursor() {2825 const hotx = this._FBU.x; // hotspot-x2826 const hoty = this._FBU.y; // hotspot-y2827 const w = this._FBU.width;2828 const h = this._FBU.height;28292830 const pixelslength = w * h * 4;2831 const masklength = Math.ceil(w / 8) * h;28322833 let bytes = pixelslength + masklength;2834 if (this._sock.rQwait("cursor encoding", bytes)) {2835 return false;2836 }28372838 // Decode from BGRX pixels + bit mask to RGBA2839 const pixels = this._sock.rQshiftBytes(pixelslength);2840 const mask = this._sock.rQshiftBytes(masklength);2841 let rgba = new Uint8Array(w * h * 4);28422843 let pixIdx = 0;2844 for (let y = 0; y < h; y++) {2845 for (let x = 0; x < w; x++) {2846 let maskIdx = y * Math.ceil(w / 8) + Math.floor(x / 8);2847 let alpha = (mask[maskIdx] << (x % 8)) & 0x80 ? 255 : 0;2848 rgba[pixIdx ] = pixels[pixIdx + 2];2849 rgba[pixIdx + 1] = pixels[pixIdx + 1];2850 rgba[pixIdx + 2] = pixels[pixIdx];2851 rgba[pixIdx + 3] = alpha;2852 pixIdx += 4;2853 }2854 }28552856 this._updateCursor(rgba, hotx, hoty, w, h);28572858 return true;2859 }28602861 _handleDesktopName() {2862 if (this._sock.rQwait("DesktopName", 4)) {2863 return false;2864 }28652866 let length = this._sock.rQshift32();28672868 if (this._sock.rQwait("DesktopName", length, 4)) {2869 return false;2870 }28712872 let name = this._sock.rQshiftStr(length);2873 name = decodeUTF8(name, true);28742875 this._setDesktopName(name);28762877 return true;2878 }28792880 _handleLedEvent() {2881 if (this._sock.rQwait("LED status", 1)) {2882 return false;2883 }28842885 let data = this._sock.rQshift8();2886 // ScrollLock state can be retrieved with data & 1. This is currently not needed.2887 let numLock = data & 2 ? true : false;2888 let capsLock = data & 4 ? true : false;2889 this._remoteCapsLock = capsLock;2890 this._remoteNumLock = numLock;28912892 return true;2893 }28942895 _handleExtendedDesktopSize() {2896 if (this._sock.rQwait("ExtendedDesktopSize", 4)) {2897 return false;2898 }28992900 const numberOfScreens = this._sock.rQpeek8();29012902 let bytes = 4 + (numberOfScreens * 16);2903 if (this._sock.rQwait("ExtendedDesktopSize", bytes)) {2904 return false;2905 }29062907 const firstUpdate = !this._supportsSetDesktopSize;2908 this._supportsSetDesktopSize = true;29092910 this._sock.rQskipBytes(1); // number-of-screens2911 this._sock.rQskipBytes(3); // padding29122913 for (let i = 0; i < numberOfScreens; i += 1) {2914 // Save the id and flags of the first screen2915 if (i === 0) {2916 this._screenID = this._sock.rQshift32(); // id2917 this._sock.rQskipBytes(2); // x-position2918 this._sock.rQskipBytes(2); // y-position2919 this._sock.rQskipBytes(2); // width2920 this._sock.rQskipBytes(2); // height2921 this._screenFlags = this._sock.rQshift32(); // flags2922 } else {2923 this._sock.rQskipBytes(16);2924 }2925 }29262927 /*2928 * The x-position indicates the reason for the change:2929 *2930 * 0 - server resized on its own2931 * 1 - this client requested the resize2932 * 2 - another client requested the resize2933 */29342935 if (this._FBU.x === 1) {2936 this._pendingRemoteResize = false;2937 }29382939 // We need to handle errors when we requested the resize.2940 if (this._FBU.x === 1 && this._FBU.y !== 0) {2941 let msg = "";2942 // The y-position indicates the status code from the server2943 switch (this._FBU.y) {2944 case 1:2945 msg = "Resize is administratively prohibited";2946 break;2947 case 2:2948 msg = "Out of resources";2949 break;2950 case 3:2951 msg = "Invalid screen layout";2952 break;2953 default:2954 msg = "Unknown reason";2955 break;2956 }2957 Log.Warn("Server did not accept the resize request: "2958 + msg);2959 } else {2960 this._resize(this._FBU.width, this._FBU.height);2961 }29622963 // Normally we only apply the current resize mode after a2964 // window resize event. However there is no such trigger on the2965 // initial connect. And we don't know if the server supports2966 // resizing until we've gotten here.2967 if (firstUpdate) {2968 this._requestRemoteResize();2969 }29702971 if (this._FBU.x === 1 && this._FBU.y === 0) {2972 // We might have resized again whilst waiting for the2973 // previous request, so check if we are in sync2974 this._requestRemoteResize();2975 }29762977 return true;2978 }29792980 _handleDataRect() {2981 let decoder = this._decoders[this._FBU.encoding];2982 if (!decoder) {2983 this._fail("Unsupported encoding (encoding: " +2984 this._FBU.encoding + ")");2985 return false;2986 }29872988 try {2989 return decoder.decodeRect(this._FBU.x, this._FBU.y,2990 this._FBU.width, this._FBU.height,2991 this._sock, this._display,2992 this._fbDepth);2993 } catch (err) {2994 this._fail("Error decoding rect: " + err);2995 return false;2996 }2997 }29982999 _updateContinuousUpdates() {3000 if (!this._enabledContinuousUpdates) { return; }30013002 RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,3003 this._fbWidth, this._fbHeight);3004 }30053006 // Handle resize-messages from the server3007 _resize(width, height) {3008 this._fbWidth = width;3009 this._fbHeight = height;30103011 this._display.resize(this._fbWidth, this._fbHeight);30123013 // Adjust the visible viewport based on the new dimensions3014 this._updateClip();3015 this._updateScale();30163017 this._updateContinuousUpdates();30183019 // Keep this size until browser client size changes3020 this._saveExpectedClientSize();3021 }30223023 _xvpOp(ver, op) {3024 if (this._rfbXvpVer < ver) { return; }3025 Log.Info("Sending XVP operation " + op + " (version " + ver + ")");3026 RFB.messages.xvpOp(this._sock, ver, op);3027 }30283029 _updateCursor(rgba, hotx, hoty, w, h) {3030 this._cursorImage = {3031 rgbaPixels: rgba,3032 hotx: hotx, hoty: hoty, w: w, h: h,3033 };3034 this._refreshCursor();3035 }30363037 _shouldShowDotCursor() {3038 // Called when this._cursorImage is updated3039 if (!this._showDotCursor) {3040 // User does not want to see the dot, so...3041 return false;3042 }30433044 // The dot should not be shown if the cursor is already visible,3045 // i.e. contains at least one not-fully-transparent pixel.3046 // So iterate through all alpha bytes in rgba and stop at the3047 // first non-zero.3048 for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) {3049 if (this._cursorImage.rgbaPixels[i]) {3050 return false;3051 }3052 }30533054 // At this point, we know that the cursor is fully transparent, and3055 // the user wants to see the dot instead of this.3056 return true;3057 }30583059 _refreshCursor() {3060 if (this._rfbConnectionState !== "connecting" &&3061 this._rfbConnectionState !== "connected") {3062 return;3063 }3064 const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage;3065 this._cursor.change(image.rgbaPixels,3066 image.hotx, image.hoty,3067 image.w, image.h3068 );3069 }30703071 static genDES(password, challenge) {3072 const passwordChars = password.split('').map(c => c.charCodeAt(0));3073 const key = legacyCrypto.importKey(3074 "raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]);3075 return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge);3076 }3077}30783079// Class Methods3080RFB.messages = {3081 keyEvent(sock, keysym, down) {3082 sock.sQpush8(4); // msg-type3083 sock.sQpush8(down);30843085 sock.sQpush16(0);30863087 sock.sQpush32(keysym);30883089 sock.flush();3090 },30913092 QEMUExtendedKeyEvent(sock, keysym, down, keycode) {3093 function getRFBkeycode(xtScanCode) {3094 const upperByte = (keycode >> 8);3095 const lowerByte = (keycode & 0x00ff);3096 if (upperByte === 0xe0 && lowerByte < 0x7f) {3097 return lowerByte | 0x80;3098 }3099 return xtScanCode;3100 }31013102 sock.sQpush8(255); // msg-type3103 sock.sQpush8(0); // sub msg-type31043105 sock.sQpush16(down);31063107 sock.sQpush32(keysym);31083109 const RFBkeycode = getRFBkeycode(keycode);31103111 sock.sQpush32(RFBkeycode);31123113 sock.flush();3114 },31153116 pointerEvent(sock, x, y, mask) {3117 sock.sQpush8(5); // msg-type31183119 // Marker bit must be set to 0, otherwise the server might3120 // confuse the marker bit with the highest bit in a normal3121 // PointerEvent message.3122 mask = mask & 0x7f;3123 sock.sQpush8(mask);31243125 sock.sQpush16(x);3126 sock.sQpush16(y);31273128 sock.flush();3129 },31303131 extendedPointerEvent(sock, x, y, mask) {3132 sock.sQpush8(5); // msg-type31333134 let higherBits = (mask >> 7) & 0xff;31353136 // Bits 2-7 are reserved3137 if (higherBits & 0xfc) {3138 throw new Error("Invalid mouse button mask: " + mask);3139 }31403141 let lowerBits = mask & 0x7f;3142 lowerBits |= 0x80; // Set marker bit to 131433144 sock.sQpush8(lowerBits);3145 sock.sQpush16(x);3146 sock.sQpush16(y);3147 sock.sQpush8(higherBits);31483149 sock.flush();3150 },31513152 // Used to build Notify and Request data.3153 _buildExtendedClipboardFlags(actions, formats) {3154 let data = new Uint8Array(4);3155 let formatFlag = 0x00000000;3156 let actionFlag = 0x00000000;31573158 for (let i = 0; i < actions.length; i++) {3159 actionFlag |= actions[i];3160 }31613162 for (let i = 0; i < formats.length; i++) {3163 formatFlag |= formats[i];3164 }31653166 data[0] = actionFlag >> 24; // Actions3167 data[1] = 0x00; // Reserved3168 data[2] = 0x00; // Reserved3169 data[3] = formatFlag; // Formats31703171 return data;3172 },31733174 extendedClipboardProvide(sock, formats, inData) {3175 // Deflate incomming data and their sizes3176 let deflator = new Deflator();3177 let dataToDeflate = [];31783179 for (let i = 0; i < formats.length; i++) {3180 // We only support the format Text at this time3181 if (formats[i] != extendedClipboardFormatText) {3182 throw new Error("Unsupported extended clipboard format for Provide message.");3183 }31843185 // Change lone \r or \n into \r\n as defined in rfbproto3186 inData[i] = inData[i].replace(/\r\n|\r|\n/gm, "\r\n");31873188 // Check if it already has \03189 let text = encodeUTF8(inData[i] + "\0");31903191 dataToDeflate.push( (text.length >> 24) & 0xFF,3192 (text.length >> 16) & 0xFF,3193 (text.length >> 8) & 0xFF,3194 (text.length & 0xFF));31953196 for (let j = 0; j < text.length; j++) {3197 dataToDeflate.push(text.charCodeAt(j));3198 }3199 }32003201 let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate));32023203 // Build data to send3204 let data = new Uint8Array(4 + deflatedData.length);3205 data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide],3206 formats));3207 data.set(deflatedData, 4);32083209 RFB.messages.clientCutText(sock, data, true);3210 },32113212 extendedClipboardNotify(sock, formats) {3213 let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify],3214 formats);3215 RFB.messages.clientCutText(sock, flags, true);3216 },32173218 extendedClipboardRequest(sock, formats) {3219 let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest],3220 formats);3221 RFB.messages.clientCutText(sock, flags, true);3222 },32233224 extendedClipboardCaps(sock, actions, formats) {3225 let formatKeys = Object.keys(formats);3226 let data = new Uint8Array(4 + (4 * formatKeys.length));32273228 formatKeys.map(x => parseInt(x));3229 formatKeys.sort((a, b) => a - b);32303231 data.set(RFB.messages._buildExtendedClipboardFlags(actions, []));32323233 let loopOffset = 4;3234 for (let i = 0; i < formatKeys.length; i++) {3235 data[loopOffset] = formats[formatKeys[i]] >> 24;3236 data[loopOffset + 1] = formats[formatKeys[i]] >> 16;3237 data[loopOffset + 2] = formats[formatKeys[i]] >> 8;3238 data[loopOffset + 3] = formats[formatKeys[i]] >> 0;32393240 loopOffset += 4;3241 data[3] |= (1 << formatKeys[i]); // Update our format flags3242 }32433244 RFB.messages.clientCutText(sock, data, true);3245 },32463247 clientCutText(sock, data, extended = false) {3248 sock.sQpush8(6); // msg-type32493250 sock.sQpush8(0); // padding3251 sock.sQpush8(0); // padding3252 sock.sQpush8(0); // padding32533254 let length;3255 if (extended) {3256 length = toUnsigned32bit(-data.length);3257 } else {3258 length = data.length;3259 }32603261 sock.sQpush32(length);3262 sock.sQpushBytes(data);3263 sock.flush();3264 },32653266 setDesktopSize(sock, width, height, id, flags) {3267 sock.sQpush8(251); // msg-type32683269 sock.sQpush8(0); // padding32703271 sock.sQpush16(width);3272 sock.sQpush16(height);32733274 sock.sQpush8(1); // number-of-screens32753276 sock.sQpush8(0); // padding32773278 // screen array3279 sock.sQpush32(id);3280 sock.sQpush16(0); // x-position3281 sock.sQpush16(0); // y-position3282 sock.sQpush16(width);3283 sock.sQpush16(height);3284 sock.sQpush32(flags);32853286 sock.flush();3287 },32883289 clientFence(sock, flags, payload) {3290 sock.sQpush8(248); // msg-type32913292 sock.sQpush8(0); // padding3293 sock.sQpush8(0); // padding3294 sock.sQpush8(0); // padding32953296 sock.sQpush32(flags);32973298 sock.sQpush8(payload.length);3299 sock.sQpushString(payload);33003301 sock.flush();3302 },33033304 enableContinuousUpdates(sock, enable, x, y, width, height) {3305 sock.sQpush8(150); // msg-type33063307 sock.sQpush8(enable);33083309 sock.sQpush16(x);3310 sock.sQpush16(y);3311 sock.sQpush16(width);3312 sock.sQpush16(height);33133314 sock.flush();3315 },33163317 pixelFormat(sock, depth, trueColor) {3318 let bpp;33193320 if (depth > 16) {3321 bpp = 32;3322 } else if (depth > 8) {3323 bpp = 16;3324 } else {3325 bpp = 8;3326 }33273328 const bits = Math.floor(depth/3);33293330 sock.sQpush8(0); // msg-type33313332 sock.sQpush8(0); // padding3333 sock.sQpush8(0); // padding3334 sock.sQpush8(0); // padding33353336 sock.sQpush8(bpp);3337 sock.sQpush8(depth);3338 sock.sQpush8(0); // little-endian3339 sock.sQpush8(trueColor ? 1 : 0);33403341 sock.sQpush16((1 << bits) - 1); // red-max3342 sock.sQpush16((1 << bits) - 1); // green-max3343 sock.sQpush16((1 << bits) - 1); // blue-max33443345 sock.sQpush8(bits * 0); // red-shift3346 sock.sQpush8(bits * 1); // green-shift3347 sock.sQpush8(bits * 2); // blue-shift33483349 sock.sQpush8(0); // padding3350 sock.sQpush8(0); // padding3351 sock.sQpush8(0); // padding33523353 sock.flush();3354 },33553356 clientEncodings(sock, encodings) {3357 sock.sQpush8(2); // msg-type33583359 sock.sQpush8(0); // padding33603361 sock.sQpush16(encodings.length);3362 for (let i = 0; i < encodings.length; i++) {3363 sock.sQpush32(encodings[i]);3364 }33653366 sock.flush();3367 },33683369 fbUpdateRequest(sock, incremental, x, y, w, h) {3370 if (typeof(x) === "undefined") { x = 0; }3371 if (typeof(y) === "undefined") { y = 0; }33723373 sock.sQpush8(3); // msg-type33743375 sock.sQpush8(incremental ? 1 : 0);33763377 sock.sQpush16(x);3378 sock.sQpush16(y);3379 sock.sQpush16(w);3380 sock.sQpush16(h);33813382 sock.flush();3383 },33843385 xvpOp(sock, ver, op) {3386 sock.sQpush8(250); // msg-type33873388 sock.sQpush8(0); // padding33893390 sock.sQpush8(ver);3391 sock.sQpush8(op);33923393 sock.flush();3394 }3395};33963397RFB.cursors = {3398 none: {3399 rgbaPixels: new Uint8Array(),3400 w: 0, h: 0,3401 hotx: 0, hoty: 0,3402 },34033404 dot: {3405 /* eslint-disable indent */3406 rgbaPixels: new Uint8Array([3407 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,3408 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255,3409 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255,3410 ]),3411 /* eslint-enable indent */3412 w: 3, h: 3,3413 hotx: 1, hoty: 1,3414 }3415};3416