main
Raw Download raw file
   1/*
   2 * noVNC: HTML5 VNC client
   3 * Copyright (C) 2020 The noVNC authors
   4 * Licensed under MPL 2.0 (see LICENSE.txt)
   5 *
   6 * See README.md for usage and integration instructions.
   7 *
   8 */
   9
  10import { 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 AsyncClipboard from "./clipboard.js";
  19import Inflator from "./inflator.js";
  20import Deflator from "./deflator.js";
  21import Keyboard from "./input/keyboard.js";
  22import GestureHandler from "./input/gesturehandler.js";
  23import Cursor from "./util/cursor.js";
  24import Websock from "./websock.js";
  25import KeyTable from "./input/keysym.js";
  26import XtScancode from "./input/xtscancodes.js";
  27import { encodings } from "./encodings.js";
  28import RSAAESAuthenticationState from "./ra2.js";
  29import legacyCrypto from "./crypto/crypto.js";
  30
  31import RawDecoder from "./decoders/raw.js";
  32import CopyRectDecoder from "./decoders/copyrect.js";
  33import RREDecoder from "./decoders/rre.js";
  34import HextileDecoder from "./decoders/hextile.js";
  35import ZlibDecoder from './decoders/zlib.js';
  36import TightDecoder from "./decoders/tight.js";
  37import TightPNGDecoder from "./decoders/tightpng.js";
  38import ZRLEDecoder from "./decoders/zrle.js";
  39import JPEGDecoder from "./decoders/jpeg.js";
  40import H264Decoder from "./decoders/h264.js";
  41
  42// How many seconds to wait for a disconnect to finish
  43const DISCONNECT_TIMEOUT = 3;
  44const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)';
  45
  46// Minimum wait (ms) between two mouse moves
  47const MOUSE_MOVE_DELAY = 17;
  48
  49// Wheel thresholds
  50const WHEEL_STEP = 50; // Pixels needed for one step
  51const WHEEL_LINE_HEIGHT = 19; // Assumed pixels for one line step
  52
  53// Gesture thresholds
  54const GESTURE_ZOOMSENS = 75;
  55const GESTURE_SCRLSENS = 50;
  56const DOUBLE_TAP_TIMEOUT = 1000;
  57const DOUBLE_TAP_THRESHOLD = 50;
  58
  59// Security types
  60const securityTypeNone              = 1;
  61const securityTypeVNCAuth           = 2;
  62const securityTypeRA2ne             = 6;
  63const securityTypeTight             = 16;
  64const securityTypeVeNCrypt          = 19;
  65const securityTypeXVP               = 22;
  66const securityTypeARD               = 30;
  67const securityTypeMSLogonII         = 113;
  68
  69// Special Tight security types
  70const securityTypeUnixLogon         = 129;
  71
  72// VeNCrypt security types
  73const securityTypePlain             = 256;
  74
  75// Extended clipboard pseudo-encoding formats
  76const extendedClipboardFormatText   = 1;
  77/*eslint-disable no-unused-vars */
  78const extendedClipboardFormatRtf    = 1 << 1;
  79const extendedClipboardFormatHtml   = 1 << 2;
  80const extendedClipboardFormatDib    = 1 << 3;
  81const extendedClipboardFormatFiles  = 1 << 4;
  82/*eslint-enable */
  83
  84// Extended clipboard pseudo-encoding actions
  85const extendedClipboardActionCaps    = 1 << 24;
  86const extendedClipboardActionRequest = 1 << 25;
  87const extendedClipboardActionPeek    = 1 << 26;
  88const extendedClipboardActionNotify  = 1 << 27;
  89const extendedClipboardActionProvide = 1 << 28;
  90
  91export default class RFB extends EventTargetMixin {
  92    constructor(target, urlOrChannel, options) {
  93        if (!target) {
  94            throw new Error("Must specify target");
  95        }
  96        if (!urlOrChannel) {
  97            throw new Error("Must specify URL, WebSocket or RTCDataChannel");
  98        }
  99
 100        // We rely on modern APIs which might not be available in an
 101        // insecure context
 102        if (!window.isSecureContext) {
 103            Log.Error("noVNC requires a secure context (TLS). Expect crashes!");
 104        }
 105
 106        super();
 107
 108        this._target = target;
 109
 110        if (typeof urlOrChannel === "string") {
 111            this._url = urlOrChannel;
 112        } else {
 113            this._url = null;
 114            this._rawChannel = urlOrChannel;
 115        }
 116
 117        // Connection details
 118        options = options || {};
 119        this._rfbCredentials = options.credentials || {};
 120        this._shared = 'shared' in options ? !!options.shared : true;
 121        this._repeaterID = options.repeaterID || '';
 122        this._wsProtocols = options.wsProtocols || [];
 123
 124        // Internal state
 125        this._rfbConnectionState = '';
 126        this._rfbInitState = '';
 127        this._rfbAuthScheme = -1;
 128        this._rfbCleanDisconnect = true;
 129        this._rfbRSAAESAuthenticationState = null;
 130
 131        // Server capabilities
 132        this._rfbVersion = 0;
 133        this._rfbMaxVersion = 3.8;
 134        this._rfbTightVNC = false;
 135        this._rfbVeNCryptState = 0;
 136        this._rfbXvpVer = 0;
 137
 138        this._fbWidth = 0;
 139        this._fbHeight = 0;
 140
 141        this._fbName = "";
 142
 143        this._capabilities = { power: false };
 144
 145        this._supportsFence = false;
 146
 147        this._supportsContinuousUpdates = false;
 148        this._enabledContinuousUpdates = false;
 149
 150        this._supportsSetDesktopSize = false;
 151        this._screenID = 0;
 152        this._screenFlags = 0;
 153        this._pendingRemoteResize = false;
 154        this._lastResize = 0;
 155
 156        this._qemuExtKeyEventSupported = false;
 157
 158        this._extendedPointerEventSupported = false;
 159
 160        this._clipboardText = null;
 161        this._clipboardServerCapabilitiesActions = {};
 162        this._clipboardServerCapabilitiesFormats = {};
 163
 164        // Internal objects
 165        this._sock = null;              // Websock object
 166        this._display = null;           // Display object
 167        this._flushing = false;         // Display flushing state
 168        this._asyncClipboard = null;    // Async clipboard object
 169        this._keyboard = null;          // Keyboard input handler object
 170        this._gestures = null;          // Gesture input handler object
 171        this._resizeObserver = null;    // Resize observer object
 172
 173        // Timers
 174        this._disconnTimer = null;      // disconnection timer
 175        this._resizeTimeout = null;     // resize rate limiting
 176        this._mouseMoveTimer = null;
 177
 178        // Decoder states
 179        this._decoders = {};
 180
 181        this._FBU = {
 182            rects: 0,
 183            x: 0,
 184            y: 0,
 185            width: 0,
 186            height: 0,
 187            encoding: null,
 188        };
 189
 190        // Mouse state
 191        this._mousePos = {};
 192        this._mouseButtonMask = 0;
 193        this._mouseLastMoveTime = 0;
 194        this._viewportDragging = false;
 195        this._viewportDragPos = {};
 196        this._viewportHasMoved = false;
 197        this._accumulatedWheelDeltaX = 0;
 198        this._accumulatedWheelDeltaY = 0;
 199
 200        // Gesture state
 201        this._gestureLastTapTime = null;
 202        this._gestureFirstDoubleTapEv = null;
 203        this._gestureLastMagnitudeX = 0;
 204        this._gestureLastMagnitudeY = 0;
 205
 206        // Bound event handlers
 207        this._eventHandlers = {
 208            focusCanvas: this._focusCanvas.bind(this),
 209            handleResize: this._handleResize.bind(this),
 210            handleMouse: this._handleMouse.bind(this),
 211            handleWheel: this._handleWheel.bind(this),
 212            handleGesture: this._handleGesture.bind(this),
 213            handleRSAAESCredentialsRequired: this._handleRSAAESCredentialsRequired.bind(this),
 214            handleRSAAESServerVerification: this._handleRSAAESServerVerification.bind(this),
 215        };
 216
 217        // main setup
 218        Log.Debug(">> RFB.constructor");
 219
 220        // Create DOM elements
 221        this._screen = document.createElement('div');
 222        this._screen.style.display = 'flex';
 223        this._screen.style.width = '100%';
 224        this._screen.style.height = '100%';
 225        this._screen.style.overflow = 'auto';
 226        this._screen.style.background = DEFAULT_BACKGROUND;
 227        this._canvas = document.createElement('canvas');
 228        this._canvas.style.margin = 'auto';
 229        // Some browsers add an outline on focus
 230        this._canvas.style.outline = 'none';
 231        this._canvas.width = 0;
 232        this._canvas.height = 0;
 233        this._canvas.tabIndex = -1;
 234        this._screen.appendChild(this._canvas);
 235
 236        // Cursor
 237        this._cursor = new Cursor();
 238
 239        // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes
 240        // it. Result: no cursor at all until a window border or an edit field
 241        // is hit blindly. But there are also VNC servers that draw the cursor
 242        // in the framebuffer and don't send the empty local cursor. There is
 243        // no way to satisfy both sides.
 244        //
 245        // The spec is unclear on this "initial cursor" issue. Many other
 246        // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the
 247        // initial cursor instead.
 248        this._cursorImage = RFB.cursors.none;
 249
 250        // populate decoder array with objects
 251        this._decoders[encodings.encodingRaw] = new RawDecoder();
 252        this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
 253        this._decoders[encodings.encodingRRE] = new RREDecoder();
 254        this._decoders[encodings.encodingHextile] = new HextileDecoder();
 255        this._decoders[encodings.encodingZlib] = new ZlibDecoder();
 256        this._decoders[encodings.encodingTight] = new TightDecoder();
 257        this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
 258        this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();
 259        this._decoders[encodings.encodingJPEG] = new JPEGDecoder();
 260        this._decoders[encodings.encodingH264] = new H264Decoder();
 261
 262        // NB: nothing that needs explicit teardown should be done
 263        // before this point, since this can throw an exception
 264        try {
 265            this._display = new Display(this._canvas);
 266        } catch (exc) {
 267            Log.Error("Display exception: " + exc);
 268            throw exc;
 269        }
 270
 271        this._asyncClipboard = new AsyncClipboard(this._canvas);
 272        this._asyncClipboard.onpaste = this.clipboardPasteFrom.bind(this);
 273
 274        this._keyboard = new Keyboard(this._canvas);
 275        this._keyboard.onkeyevent = this._handleKeyEvent.bind(this);
 276        this._remoteCapsLock = null; // Null indicates unknown or irrelevant
 277        this._remoteNumLock = null;
 278
 279        this._gestures = new GestureHandler();
 280
 281        this._sock = new Websock();
 282        this._sock.on('open', this._socketOpen.bind(this));
 283        this._sock.on('close', this._socketClose.bind(this));
 284        this._sock.on('message', this._handleMessage.bind(this));
 285        this._sock.on('error', this._socketError.bind(this));
 286
 287        this._expectedClientWidth = null;
 288        this._expectedClientHeight = null;
 289        this._resizeObserver = new ResizeObserver(this._eventHandlers.handleResize);
 290
 291        // All prepared, kick off the connection
 292        this._updateConnectionState('connecting');
 293
 294        Log.Debug("<< RFB.constructor");
 295
 296        // ===== PROPERTIES =====
 297
 298        this.dragViewport = false;
 299        this.focusOnClick = true;
 300
 301        this._viewOnly = false;
 302        this._clipViewport = false;
 303        this._clippingViewport = false;
 304        this._scaleViewport = false;
 305        this._resizeSession = false;
 306
 307        this._showDotCursor = false;
 308
 309        this._qualityLevel = 6;
 310        this._compressionLevel = 2;
 311    }
 312
 313    // ===== PROPERTIES =====
 314
 315    get viewOnly() { return this._viewOnly; }
 316    set viewOnly(viewOnly) {
 317        this._viewOnly = viewOnly;
 318
 319        if (this._rfbConnectionState === "connecting" ||
 320            this._rfbConnectionState === "connected") {
 321            if (viewOnly) {
 322                this._keyboard.ungrab();
 323                this._asyncClipboard.ungrab();
 324            } else {
 325                this._keyboard.grab();
 326                this._asyncClipboard.grab();
 327            }
 328        }
 329    }
 330
 331    get capabilities() { return this._capabilities; }
 332
 333    get clippingViewport() { return this._clippingViewport; }
 334    _setClippingViewport(on) {
 335        if (on === this._clippingViewport) {
 336            return;
 337        }
 338        this._clippingViewport = on;
 339        this.dispatchEvent(new CustomEvent("clippingviewport",
 340                                           { detail: this._clippingViewport }));
 341    }
 342
 343    get touchButton() { return 0; }
 344    set touchButton(button) { Log.Warn("Using old API!"); }
 345
 346    get clipViewport() { return this._clipViewport; }
 347    set clipViewport(viewport) {
 348        this._clipViewport = viewport;
 349        this._updateClip();
 350    }
 351
 352    get scaleViewport() { return this._scaleViewport; }
 353    set scaleViewport(scale) {
 354        this._scaleViewport = scale;
 355        // Scaling trumps clipping, so we may need to adjust
 356        // clipping when enabling or disabling scaling
 357        if (scale && this._clipViewport) {
 358            this._updateClip();
 359        }
 360        this._updateScale();
 361        if (!scale && this._clipViewport) {
 362            this._updateClip();
 363        }
 364    }
 365
 366    get resizeSession() { return this._resizeSession; }
 367    set resizeSession(resize) {
 368        this._resizeSession = resize;
 369        if (resize) {
 370            this._requestRemoteResize();
 371        }
 372    }
 373
 374    get showDotCursor() { return this._showDotCursor; }
 375    set showDotCursor(show) {
 376        this._showDotCursor = show;
 377        this._refreshCursor();
 378    }
 379
 380    get background() { return this._screen.style.background; }
 381    set background(cssValue) { this._screen.style.background = cssValue; }
 382
 383    get qualityLevel() {
 384        return this._qualityLevel;
 385    }
 386    set qualityLevel(qualityLevel) {
 387        if (!Number.isInteger(qualityLevel) || qualityLevel < 0 || qualityLevel > 9) {
 388            Log.Error("qualityLevel must be an integer between 0 and 9");
 389            return;
 390        }
 391
 392        if (this._qualityLevel === qualityLevel) {
 393            return;
 394        }
 395
 396        this._qualityLevel = qualityLevel;
 397
 398        if (this._rfbConnectionState === 'connected') {
 399            this._sendEncodings();
 400        }
 401    }
 402
 403    get compressionLevel() {
 404        return this._compressionLevel;
 405    }
 406    set compressionLevel(compressionLevel) {
 407        if (!Number.isInteger(compressionLevel) || compressionLevel < 0 || compressionLevel > 9) {
 408            Log.Error("compressionLevel must be an integer between 0 and 9");
 409            return;
 410        }
 411
 412        if (this._compressionLevel === compressionLevel) {
 413            return;
 414        }
 415
 416        this._compressionLevel = compressionLevel;
 417
 418        if (this._rfbConnectionState === 'connected') {
 419            this._sendEncodings();
 420        }
 421    }
 422
 423    // ===== PUBLIC METHODS =====
 424
 425    disconnect() {
 426        this._updateConnectionState('disconnecting');
 427        this._sock.off('error');
 428        this._sock.off('message');
 429        this._sock.off('open');
 430        if (this._rfbRSAAESAuthenticationState !== null) {
 431            this._rfbRSAAESAuthenticationState.disconnect();
 432        }
 433    }
 434
 435    approveServer() {
 436        if (this._rfbRSAAESAuthenticationState !== null) {
 437            this._rfbRSAAESAuthenticationState.approveServer();
 438        }
 439    }
 440
 441    sendCredentials(creds) {
 442        this._rfbCredentials = creds;
 443        this._resumeAuthentication();
 444    }
 445
 446    sendCtrlAltDel() {
 447        if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
 448        Log.Info("Sending Ctrl-Alt-Del");
 449
 450        this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true);
 451        this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true);
 452        this.sendKey(KeyTable.XK_Delete, "Delete", true);
 453        this.sendKey(KeyTable.XK_Delete, "Delete", false);
 454        this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false);
 455        this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false);
 456    }
 457
 458    machineShutdown() {
 459        this._xvpOp(1, 2);
 460    }
 461
 462    machineReboot() {
 463        this._xvpOp(1, 3);
 464    }
 465
 466    machineReset() {
 467        this._xvpOp(1, 4);
 468    }
 469
 470    // Send a key press. If 'down' is not specified then send a down key
 471    // followed by an up key.
 472    sendKey(keysym, code, down) {
 473        if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
 474
 475        if (down === undefined) {
 476            this.sendKey(keysym, code, true);
 477            this.sendKey(keysym, code, false);
 478            return;
 479        }
 480
 481        const scancode = XtScancode[code];
 482
 483        if (this._qemuExtKeyEventSupported && scancode) {
 484            // 0 is NoSymbol
 485            keysym = keysym || 0;
 486
 487            Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode);
 488
 489            RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode);
 490        } else {
 491            if (!keysym) {
 492                return;
 493            }
 494            Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
 495            RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
 496        }
 497    }
 498
 499    focus(options) {
 500        this._canvas.focus(options);
 501    }
 502
 503    blur() {
 504        this._canvas.blur();
 505    }
 506
 507    clipboardPasteFrom(text) {
 508        if (this._rfbConnectionState !== 'connected' || this._viewOnly) { return; }
 509
 510        if (this._clipboardServerCapabilitiesFormats[extendedClipboardFormatText] &&
 511            this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
 512
 513            this._clipboardText = text;
 514            RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
 515        } else {
 516            let length, i;
 517            let data;
 518
 519            length = 0;
 520            // eslint-disable-next-line no-unused-vars
 521            for (let codePoint of text) {
 522                length++;
 523            }
 524
 525            data = new Uint8Array(length);
 526
 527            i = 0;
 528            for (let codePoint of text) {
 529                let code = codePoint.codePointAt(0);
 530
 531                /* Only ISO 8859-1 is supported */
 532                if (code > 0xff) {
 533                    code = 0x3f; // '?'
 534                }
 535
 536                data[i++] = code;
 537            }
 538
 539            RFB.messages.clientCutText(this._sock, data);
 540        }
 541    }
 542
 543    getImageData() {
 544        return this._display.getImageData();
 545    }
 546
 547    toDataURL(type, encoderOptions) {
 548        return this._display.toDataURL(type, encoderOptions);
 549    }
 550
 551    toBlob(callback, type, quality) {
 552        return this._display.toBlob(callback, type, quality);
 553    }
 554
 555    // ===== PRIVATE METHODS =====
 556
 557    _connect() {
 558        Log.Debug(">> RFB.connect");
 559
 560        if (this._url) {
 561            Log.Info(`connecting to ${this._url}`);
 562            this._sock.open(this._url, this._wsProtocols);
 563        } else {
 564            Log.Info(`attaching ${this._rawChannel} to Websock`);
 565            this._sock.attach(this._rawChannel);
 566
 567            if (this._sock.readyState === 'closed') {
 568                throw Error("Cannot use already closed WebSocket/RTCDataChannel");
 569            }
 570
 571            if (this._sock.readyState === 'open') {
 572                // FIXME: _socketOpen() can in theory call _fail(), which
 573                //        isn't allowed this early, but I'm not sure that can
 574                //        happen without a bug messing up our state variables
 575                this._socketOpen();
 576            }
 577        }
 578
 579        // Make our elements part of the page
 580        this._target.appendChild(this._screen);
 581
 582        this._gestures.attach(this._canvas);
 583
 584        this._cursor.attach(this._canvas);
 585        this._refreshCursor();
 586
 587        // Monitor size changes of the screen element
 588        this._resizeObserver.observe(this._screen);
 589
 590        // Always grab focus on some kind of click event
 591        this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas);
 592        this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas);
 593
 594        // Mouse events
 595        this._canvas.addEventListener('mousedown', this._eventHandlers.handleMouse);
 596        this._canvas.addEventListener('mouseup', this._eventHandlers.handleMouse);
 597        this._canvas.addEventListener('mousemove', this._eventHandlers.handleMouse);
 598        // Prevent middle-click pasting (see handler for why we bind to document)
 599        this._canvas.addEventListener('click', this._eventHandlers.handleMouse);
 600        // preventDefault() on mousedown doesn't stop this event for some
 601        // reason so we have to explicitly block it
 602        this._canvas.addEventListener('contextmenu', this._eventHandlers.handleMouse);
 603
 604        // Wheel events
 605        this._canvas.addEventListener("wheel", this._eventHandlers.handleWheel);
 606
 607        // Gesture events
 608        this._canvas.addEventListener("gesturestart", this._eventHandlers.handleGesture);
 609        this._canvas.addEventListener("gesturemove", this._eventHandlers.handleGesture);
 610        this._canvas.addEventListener("gestureend", this._eventHandlers.handleGesture);
 611
 612        Log.Debug("<< RFB.connect");
 613    }
 614
 615    _disconnect() {
 616        Log.Debug(">> RFB.disconnect");
 617        this._cursor.detach();
 618        this._canvas.removeEventListener("gesturestart", this._eventHandlers.handleGesture);
 619        this._canvas.removeEventListener("gesturemove", this._eventHandlers.handleGesture);
 620        this._canvas.removeEventListener("gestureend", this._eventHandlers.handleGesture);
 621        this._canvas.removeEventListener("wheel", this._eventHandlers.handleWheel);
 622        this._canvas.removeEventListener('mousedown', this._eventHandlers.handleMouse);
 623        this._canvas.removeEventListener('mouseup', this._eventHandlers.handleMouse);
 624        this._canvas.removeEventListener('mousemove', this._eventHandlers.handleMouse);
 625        this._canvas.removeEventListener('click', this._eventHandlers.handleMouse);
 626        this._canvas.removeEventListener('contextmenu', this._eventHandlers.handleMouse);
 627        this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas);
 628        this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas);
 629        this._resizeObserver.disconnect();
 630        this._keyboard.ungrab();
 631        this._gestures.detach();
 632        this._sock.close();
 633        try {
 634            this._target.removeChild(this._screen);
 635        } catch (e) {
 636            if (e.name === 'NotFoundError') {
 637                // Some cases where the initial connection fails
 638                // can disconnect before the _screen is created
 639            } else {
 640                throw e;
 641            }
 642        }
 643        clearTimeout(this._resizeTimeout);
 644        clearTimeout(this._mouseMoveTimer);
 645        Log.Debug("<< RFB.disconnect");
 646    }
 647
 648    _socketOpen() {
 649        if ((this._rfbConnectionState === 'connecting') &&
 650            (this._rfbInitState === '')) {
 651            this._rfbInitState = 'ProtocolVersion';
 652            Log.Debug("Starting VNC handshake");
 653        } else {
 654            this._fail("Unexpected server connection while " +
 655                       this._rfbConnectionState);
 656        }
 657    }
 658
 659    _socketClose(e) {
 660        Log.Debug("WebSocket on-close event");
 661        let msg = "";
 662        if (e.code) {
 663            msg = "(code: " + e.code;
 664            if (e.reason) {
 665                msg += ", reason: " + e.reason;
 666            }
 667            msg += ")";
 668        }
 669        switch (this._rfbConnectionState) {
 670            case 'connecting':
 671                this._fail("Connection closed " + msg);
 672                break;
 673            case 'connected':
 674                // Handle disconnects that were initiated server-side
 675                this._updateConnectionState('disconnecting');
 676                this._updateConnectionState('disconnected');
 677                break;
 678            case 'disconnecting':
 679                // Normal disconnection path
 680                this._updateConnectionState('disconnected');
 681                break;
 682            case 'disconnected':
 683                this._fail("Unexpected server disconnect " +
 684                           "when already disconnected " + msg);
 685                break;
 686            default:
 687                this._fail("Unexpected server disconnect before connecting " +
 688                           msg);
 689                break;
 690        }
 691        this._sock.off('close');
 692        // Delete reference to raw channel to allow cleanup.
 693        this._rawChannel = null;
 694    }
 695
 696    _socketError(e) {
 697        Log.Warn("WebSocket on-error event");
 698    }
 699
 700    _focusCanvas(event) {
 701        if (!this.focusOnClick) {
 702            return;
 703        }
 704
 705        this.focus({ preventScroll: true });
 706    }
 707
 708    _setDesktopName(name) {
 709        this._fbName = name;
 710        this.dispatchEvent(new CustomEvent(
 711            "desktopname",
 712            { detail: { name: this._fbName } }));
 713    }
 714
 715    _saveExpectedClientSize() {
 716        this._expectedClientWidth = this._screen.clientWidth;
 717        this._expectedClientHeight = this._screen.clientHeight;
 718    }
 719
 720    _currentClientSize() {
 721        return [this._screen.clientWidth, this._screen.clientHeight];
 722    }
 723
 724    _clientHasExpectedSize() {
 725        const [currentWidth, currentHeight] = this._currentClientSize();
 726        return currentWidth == this._expectedClientWidth &&
 727            currentHeight == this._expectedClientHeight;
 728    }
 729
 730    // Handle browser window resizes
 731    _handleResize() {
 732        // Don't change anything if the client size is already as expected
 733        if (this._clientHasExpectedSize()) {
 734            return;
 735        }
 736        // If the window resized then our screen element might have
 737        // as well. Update the viewport dimensions.
 738        window.requestAnimationFrame(() => {
 739            this._updateClip();
 740            this._updateScale();
 741            this._saveExpectedClientSize();
 742        });
 743
 744        // Request changing the resolution of the remote display to
 745        // the size of the local browser viewport.
 746        this._requestRemoteResize();
 747    }
 748
 749    // Update state of clipping in Display object, and make sure the
 750    // configured viewport matches the current screen size
 751    _updateClip() {
 752        const curClip = this._display.clipViewport;
 753        let newClip = this._clipViewport;
 754
 755        if (this._scaleViewport) {
 756            // Disable viewport clipping if we are scaling
 757            newClip = false;
 758        }
 759
 760        if (curClip !== newClip) {
 761            this._display.clipViewport = newClip;
 762        }
 763
 764        if (newClip) {
 765            // When clipping is enabled, the screen is limited to
 766            // the size of the container.
 767            const size = this._screenSize();
 768            this._display.viewportChangeSize(size.w, size.h);
 769            this._fixScrollbars();
 770            this._setClippingViewport(size.w < this._display.width ||
 771                                      size.h < this._display.height);
 772        } else {
 773            this._setClippingViewport(false);
 774        }
 775
 776        // When changing clipping we might show or hide scrollbars.
 777        // This causes the expected client dimensions to change.
 778        if (curClip !== newClip) {
 779            this._saveExpectedClientSize();
 780        }
 781    }
 782
 783    _updateScale() {
 784        if (!this._scaleViewport) {
 785            this._display.scale = 1.0;
 786        } else {
 787            const size = this._screenSize();
 788            this._display.autoscale(size.w, size.h);
 789        }
 790        this._fixScrollbars();
 791    }
 792
 793    // Requests a change of remote desktop size. This message is an extension
 794    // and may only be sent if we have received an ExtendedDesktopSize message
 795    _requestRemoteResize() {
 796        if (!this._resizeSession) {
 797            return;
 798        }
 799        if (this._viewOnly) {
 800            return;
 801        }
 802        if (!this._supportsSetDesktopSize) {
 803            return;
 804        }
 805
 806        // Rate limit to one pending resize at a time
 807        if (this._pendingRemoteResize) {
 808            return;
 809        }
 810
 811        // And no more than once every 100ms
 812        if ((Date.now() - this._lastResize) < 100) {
 813            clearTimeout(this._resizeTimeout);
 814            this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this),
 815                                             100 - (Date.now() - this._lastResize));
 816            return;
 817        }
 818        this._resizeTimeout = null;
 819
 820        const size = this._screenSize();
 821
 822        // Do we actually change anything?
 823        if (size.w === this._fbWidth && size.h === this._fbHeight) {
 824            return;
 825        }
 826
 827        this._pendingRemoteResize = true;
 828        this._lastResize = Date.now();
 829        RFB.messages.setDesktopSize(this._sock,
 830                                    Math.floor(size.w), Math.floor(size.h),
 831                                    this._screenID, this._screenFlags);
 832
 833        Log.Debug('Requested new desktop size: ' +
 834                   size.w + 'x' + size.h);
 835    }
 836
 837    // Gets the the size of the available screen
 838    _screenSize() {
 839        let r = this._screen.getBoundingClientRect();
 840        return { w: r.width, h: r.height };
 841    }
 842
 843    _fixScrollbars() {
 844        // This is a hack because Safari on macOS screws up the calculation
 845        // for when scrollbars are needed. We get scrollbars when making the
 846        // browser smaller, despite remote resize being enabled. So to fix it
 847        // we temporarily toggle them off and on.
 848        const orig = this._screen.style.overflow;
 849        this._screen.style.overflow = 'hidden';
 850        // Force Safari to recalculate the layout by asking for
 851        // an element's dimensions
 852        this._screen.getBoundingClientRect();
 853        this._screen.style.overflow = orig;
 854    }
 855
 856    /*
 857     * Connection states:
 858     *   connecting
 859     *   connected
 860     *   disconnecting
 861     *   disconnected - permanent state
 862     */
 863    _updateConnectionState(state) {
 864        const oldstate = this._rfbConnectionState;
 865
 866        if (state === oldstate) {
 867            Log.Debug("Already in state '" + state + "', ignoring");
 868            return;
 869        }
 870
 871        // The 'disconnected' state is permanent for each RFB object
 872        if (oldstate === 'disconnected') {
 873            Log.Error("Tried changing state of a disconnected RFB object");
 874            return;
 875        }
 876
 877        // Ensure proper transitions before doing anything
 878        switch (state) {
 879            case 'connected':
 880                if (oldstate !== 'connecting') {
 881                    Log.Error("Bad transition to connected state, " +
 882                               "previous connection state: " + oldstate);
 883                    return;
 884                }
 885                break;
 886
 887            case 'disconnected':
 888                if (oldstate !== 'disconnecting') {
 889                    Log.Error("Bad transition to disconnected state, " +
 890                               "previous connection state: " + oldstate);
 891                    return;
 892                }
 893                break;
 894
 895            case 'connecting':
 896                if (oldstate !== '') {
 897                    Log.Error("Bad transition to connecting state, " +
 898                               "previous connection state: " + oldstate);
 899                    return;
 900                }
 901                break;
 902
 903            case 'disconnecting':
 904                if (oldstate !== 'connected' && oldstate !== 'connecting') {
 905                    Log.Error("Bad transition to disconnecting state, " +
 906                               "previous connection state: " + oldstate);
 907                    return;
 908                }
 909                break;
 910
 911            default:
 912                Log.Error("Unknown connection state: " + state);
 913                return;
 914        }
 915
 916        // State change actions
 917
 918        this._rfbConnectionState = state;
 919
 920        Log.Debug("New state '" + state + "', was '" + oldstate + "'.");
 921
 922        if (this._disconnTimer && state !== 'disconnecting') {
 923            Log.Debug("Clearing disconnect timer");
 924            clearTimeout(this._disconnTimer);
 925            this._disconnTimer = null;
 926
 927            // make sure we don't get a double event
 928            this._sock.off('close');
 929        }
 930
 931        switch (state) {
 932            case 'connecting':
 933                this._connect();
 934                break;
 935
 936            case 'connected':
 937                this.dispatchEvent(new CustomEvent("connect", { detail: {} }));
 938                break;
 939
 940            case 'disconnecting':
 941                this._disconnect();
 942
 943                this._disconnTimer = setTimeout(() => {
 944                    Log.Error("Disconnection timed out.");
 945                    this._updateConnectionState('disconnected');
 946                }, DISCONNECT_TIMEOUT * 1000);
 947                break;
 948
 949            case 'disconnected':
 950                this.dispatchEvent(new CustomEvent(
 951                    "disconnect", { detail:
 952                                    { clean: this._rfbCleanDisconnect } }));
 953                break;
 954        }
 955    }
 956
 957    /* Print errors and disconnect
 958     *
 959     * The parameter 'details' is used for information that
 960     * should be logged but not sent to the user interface.
 961     */
 962    _fail(details) {
 963        switch (this._rfbConnectionState) {
 964            case 'disconnecting':
 965                Log.Error("Failed when disconnecting: " + details);
 966                break;
 967            case 'connected':
 968                Log.Error("Failed while connected: " + details);
 969                break;
 970            case 'connecting':
 971                Log.Error("Failed when connecting: " + details);
 972                break;
 973            default:
 974                Log.Error("RFB failure: " + details);
 975                break;
 976        }
 977        this._rfbCleanDisconnect = false; //This is sent to the UI
 978
 979        // Transition to disconnected without waiting for socket to close
 980        this._updateConnectionState('disconnecting');
 981        this._updateConnectionState('disconnected');
 982
 983        return false;
 984    }
 985
 986    _setCapability(cap, val) {
 987        this._capabilities[cap] = val;
 988        this.dispatchEvent(new CustomEvent("capabilities",
 989                                           { detail: { capabilities: this._capabilities } }));
 990    }
 991
 992    _handleMessage() {
 993        if (this._sock.rQwait("message", 1)) {
 994            Log.Warn("handleMessage called on an empty receive queue");
 995            return;
 996        }
 997
 998        switch (this._rfbConnectionState) {
 999            case 'disconnected':
1000                Log.Error("Got data while disconnected");
1001                break;
1002            case 'connected':
1003                while (true) {
1004                    if (this._flushing) {
1005                        break;
1006                    }
1007                    if (!this._normalMsg()) {
1008                        break;
1009                    }
1010                    if (this._sock.rQwait("message", 1)) {
1011                        break;
1012                    }
1013                }
1014                break;
1015            case 'connecting':
1016                while (this._rfbConnectionState === 'connecting') {
1017                    if (!this._initMsg()) {
1018                        break;
1019                    }
1020                }
1021                break;
1022            default:
1023                Log.Error("Got data while in an invalid state");
1024                break;
1025        }
1026    }
1027
1028    _handleKeyEvent(keysym, code, down, numlock, capslock) {
1029        // If remote state of capslock is known, and it doesn't match the local led state of
1030        // the keyboard, we send a capslock keypress first to bring it into sync.
1031        // If we just pressed CapsLock, or we toggled it remotely due to it being out of sync
1032        // we clear the remote state so that we don't send duplicate or spurious fixes,
1033        // since it may take some time to receive the new remote CapsLock state.
1034        if (code == 'CapsLock' && down) {
1035            this._remoteCapsLock = null;
1036        }
1037        if (this._remoteCapsLock !== null && capslock !== null && this._remoteCapsLock !== capslock && down) {
1038            Log.Debug("Fixing remote caps lock");
1039
1040            this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', true);
1041            this.sendKey(KeyTable.XK_Caps_Lock, 'CapsLock', false);
1042            // We clear the remote capsLock state when we do this to prevent issues with doing this twice
1043            // before we receive an update of the the remote state.
1044            this._remoteCapsLock = null;
1045        }
1046
1047        // Logic for numlock is exactly the same.
1048        if (code == 'NumLock' && down) {
1049            this._remoteNumLock = null;
1050        }
1051        if (this._remoteNumLock !== null && numlock !== null && this._remoteNumLock !== numlock && down) {
1052            Log.Debug("Fixing remote num lock");
1053            this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', true);
1054            this.sendKey(KeyTable.XK_Num_Lock, 'NumLock', false);
1055            this._remoteNumLock = null;
1056        }
1057        this.sendKey(keysym, code, down);
1058    }
1059
1060    static _convertButtonMask(buttons) {
1061        /* The bits in MouseEvent.buttons property correspond
1062         * to the following mouse buttons:
1063         *     0: Left
1064         *     1: Right
1065         *     2: Middle
1066         *     3: Back
1067         *     4: Forward
1068         *
1069         * These bits needs to be converted to what they are defined as
1070         * in the RFB protocol.
1071         */
1072
1073        const buttonMaskMap = {
1074            0: 1 << 0, // Left
1075            1: 1 << 2, // Right
1076            2: 1 << 1, // Middle
1077            3: 1 << 7, // Back
1078            4: 1 << 8, // Forward
1079        };
1080
1081        let bmask = 0;
1082        for (let i = 0; i < 5; i++) {
1083            if (buttons & (1 << i)) {
1084                bmask |= buttonMaskMap[i];
1085            }
1086        }
1087        return bmask;
1088    }
1089
1090    _handleMouse(ev) {
1091        /*
1092         * We don't check connection status or viewOnly here as the
1093         * mouse events might be used to control the viewport
1094         */
1095
1096        if (ev.type === 'click') {
1097            /*
1098             * Note: This is only needed for the 'click' event as it fails
1099             *       to fire properly for the target element so we have
1100             *       to listen on the document element instead.
1101             */
1102            if (ev.target !== this._canvas) {
1103                return;
1104            }
1105        }
1106
1107        // FIXME: if we're in view-only and not dragging,
1108        //        should we stop events?
1109        ev.stopPropagation();
1110        ev.preventDefault();
1111
1112        if ((ev.type === 'click') || (ev.type === 'contextmenu')) {
1113            return;
1114        }
1115
1116        let pos = clientToElement(ev.clientX, ev.clientY,
1117                                  this._canvas);
1118
1119        let bmask = RFB._convertButtonMask(ev.buttons);
1120
1121        let down = ev.type == 'mousedown';
1122        switch (ev.type) {
1123            case 'mousedown':
1124            case 'mouseup':
1125                if (this.dragViewport) {
1126                    if (down && !this._viewportDragging) {
1127                        this._viewportDragging = true;
1128                        this._viewportDragPos = {'x': pos.x, 'y': pos.y};
1129                        this._viewportHasMoved = false;
1130
1131                        this._flushMouseMoveTimer(pos.x, pos.y);
1132
1133                        // Skip sending mouse events, instead save the current
1134                        // mouse mask so we can send it later.
1135                        this._mouseButtonMask = bmask;
1136                        break;
1137                    } else {
1138                        this._viewportDragging = false;
1139
1140                        // If we actually performed a drag then we are done
1141                        // here and should not send any mouse events
1142                        if (this._viewportHasMoved) {
1143                            this._mouseButtonMask = bmask;
1144                            break;
1145                        }
1146                        // Otherwise we treat this as a mouse click event.
1147                        // Send the previously saved button mask, followed
1148                        // by the current button mask at the end of this
1149                        // function.
1150                        this._sendMouse(pos.x, pos.y,  this._mouseButtonMask);
1151                    }
1152                }
1153                if (down) {
1154                    setCapture(this._canvas);
1155                }
1156                this._handleMouseButton(pos.x, pos.y, bmask);
1157                break;
1158            case 'mousemove':
1159                if (this._viewportDragging) {
1160                    const deltaX = this._viewportDragPos.x - pos.x;
1161                    const deltaY = this._viewportDragPos.y - pos.y;
1162
1163                    if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
1164                                                   Math.abs(deltaY) > dragThreshold)) {
1165                        this._viewportHasMoved = true;
1166
1167                        this._viewportDragPos = {'x': pos.x, 'y': pos.y};
1168                        this._display.viewportChangePos(deltaX, deltaY);
1169                    }
1170
1171                    // Skip sending mouse events
1172                    break;
1173                }
1174                this._handleMouseMove(pos.x, pos.y);
1175                break;
1176        }
1177    }
1178
1179    _handleMouseButton(x, y, bmask) {
1180        // Flush waiting move event first
1181        this._flushMouseMoveTimer(x, y);
1182
1183        this._mouseButtonMask = bmask;
1184        this._sendMouse(x, y, this._mouseButtonMask);
1185    }
1186
1187    _handleMouseMove(x, y) {
1188        this._mousePos = { 'x': x, 'y': y };
1189
1190        // Limit many mouse move events to one every MOUSE_MOVE_DELAY ms
1191        if (this._mouseMoveTimer == null) {
1192
1193            const timeSinceLastMove = Date.now() - this._mouseLastMoveTime;
1194            if (timeSinceLastMove > MOUSE_MOVE_DELAY) {
1195                this._sendMouse(x, y, this._mouseButtonMask);
1196                this._mouseLastMoveTime = Date.now();
1197            } else {
1198                // Too soon since the latest move, wait the remaining time
1199                this._mouseMoveTimer = setTimeout(() => {
1200                    this._handleDelayedMouseMove();
1201                }, MOUSE_MOVE_DELAY - timeSinceLastMove);
1202            }
1203        }
1204    }
1205
1206    _handleDelayedMouseMove() {
1207        this._mouseMoveTimer = null;
1208        this._sendMouse(this._mousePos.x, this._mousePos.y,
1209                        this._mouseButtonMask);
1210        this._mouseLastMoveTime = Date.now();
1211    }
1212
1213    _sendMouse(x, y, mask) {
1214        if (this._rfbConnectionState !== 'connected') { return; }
1215        if (this._viewOnly) { return; } // View only, skip mouse events
1216
1217        // Highest bit in mask is never sent to the server
1218        if (mask & 0x8000) {
1219            throw new Error("Illegal mouse button mask (mask: " + mask + ")");
1220        }
1221
1222        let extendedMouseButtons = mask & 0x7f80;
1223
1224        if (this._extendedPointerEventSupported && extendedMouseButtons) {
1225            RFB.messages.extendedPointerEvent(this._sock, this._display.absX(x),
1226                                              this._display.absY(y), mask);
1227        } else {
1228            RFB.messages.pointerEvent(this._sock, this._display.absX(x),
1229                                      this._display.absY(y), mask);
1230        }
1231    }
1232
1233    _handleWheel(ev) {
1234        if (this._rfbConnectionState !== 'connected') { return; }
1235        if (this._viewOnly) { return; } // View only, skip mouse events
1236
1237        ev.stopPropagation();
1238        ev.preventDefault();
1239
1240        let pos = clientToElement(ev.clientX, ev.clientY,
1241                                  this._canvas);
1242
1243        let bmask = RFB._convertButtonMask(ev.buttons);
1244        let dX = ev.deltaX;
1245        let dY = ev.deltaY;
1246
1247        // Pixel units unless it's non-zero.
1248        // Note that if deltamode is line or page won't matter since we aren't
1249        // sending the mouse wheel delta to the server anyway.
1250        // The difference between pixel and line can be important however since
1251        // we have a threshold that can be smaller than the line height.
1252        if (ev.deltaMode !== 0) {
1253            dX *= WHEEL_LINE_HEIGHT;
1254            dY *= WHEEL_LINE_HEIGHT;
1255        }
1256
1257        // Mouse wheel events are sent in steps over VNC. This means that the VNC
1258        // protocol can't handle a wheel event with specific distance or speed.
1259        // Therefor, if we get a lot of small mouse wheel events we combine them.
1260        this._accumulatedWheelDeltaX += dX;
1261        this._accumulatedWheelDeltaY += dY;
1262
1263
1264        // Generate a mouse wheel step event when the accumulated delta
1265        // for one of the axes is large enough.
1266        if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) {
1267            if (this._accumulatedWheelDeltaX < 0) {
1268                this._handleMouseButton(pos.x, pos.y, bmask | 1 << 5);
1269                this._handleMouseButton(pos.x, pos.y, bmask);
1270            } else if (this._accumulatedWheelDeltaX > 0) {
1271                this._handleMouseButton(pos.x, pos.y, bmask | 1 << 6);
1272                this._handleMouseButton(pos.x, pos.y, bmask);
1273            }
1274
1275            this._accumulatedWheelDeltaX = 0;
1276        }
1277        if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) {
1278            if (this._accumulatedWheelDeltaY < 0) {
1279                this._handleMouseButton(pos.x, pos.y, bmask | 1 << 3);
1280                this._handleMouseButton(pos.x, pos.y, bmask);
1281            } else if (this._accumulatedWheelDeltaY > 0) {
1282                this._handleMouseButton(pos.x, pos.y, bmask | 1 << 4);
1283                this._handleMouseButton(pos.x, pos.y, bmask);
1284            }
1285
1286            this._accumulatedWheelDeltaY = 0;
1287        }
1288    }
1289
1290    _fakeMouseMove(ev, elementX, elementY) {
1291        this._handleMouseMove(elementX, elementY);
1292        this._cursor.move(ev.detail.clientX, ev.detail.clientY);
1293    }
1294
1295    _handleTapEvent(ev, bmask) {
1296        let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
1297                                  this._canvas);
1298
1299        // If the user quickly taps multiple times we assume they meant to
1300        // hit the same spot, so slightly adjust coordinates
1301
1302        if ((this._gestureLastTapTime !== null) &&
1303            ((Date.now() - this._gestureLastTapTime) < DOUBLE_TAP_TIMEOUT) &&
1304            (this._gestureFirstDoubleTapEv.detail.type === ev.detail.type)) {
1305            let dx = this._gestureFirstDoubleTapEv.detail.clientX - ev.detail.clientX;
1306            let dy = this._gestureFirstDoubleTapEv.detail.clientY - ev.detail.clientY;
1307            let distance = Math.hypot(dx, dy);
1308
1309            if (distance < DOUBLE_TAP_THRESHOLD) {
1310                pos = clientToElement(this._gestureFirstDoubleTapEv.detail.clientX,
1311                                      this._gestureFirstDoubleTapEv.detail.clientY,
1312                                      this._canvas);
1313            } else {
1314                this._gestureFirstDoubleTapEv = ev;
1315            }
1316        } else {
1317            this._gestureFirstDoubleTapEv = ev;
1318        }
1319        this._gestureLastTapTime = Date.now();
1320
1321        this._fakeMouseMove(this._gestureFirstDoubleTapEv, pos.x, pos.y);
1322        this._handleMouseButton(pos.x, pos.y, bmask);
1323        this._handleMouseButton(pos.x, pos.y, 0x0);
1324    }
1325
1326    _handleGesture(ev) {
1327        let magnitude;
1328
1329        let pos = clientToElement(ev.detail.clientX, ev.detail.clientY,
1330                                  this._canvas);
1331        switch (ev.type) {
1332            case 'gesturestart':
1333                switch (ev.detail.type) {
1334                    case 'onetap':
1335                        this._handleTapEvent(ev, 0x1);
1336                        break;
1337                    case 'twotap':
1338                        this._handleTapEvent(ev, 0x4);
1339                        break;
1340                    case 'threetap':
1341                        this._handleTapEvent(ev, 0x2);
1342                        break;
1343                    case 'drag':
1344                        if (this.dragViewport) {
1345                            this._viewportHasMoved = false;
1346                            this._viewportDragging = true;
1347                            this._viewportDragPos = {'x': pos.x, 'y': pos.y};
1348                        } else {
1349                            this._fakeMouseMove(ev, pos.x, pos.y);
1350                            this._handleMouseButton(pos.x, pos.y, 0x1);
1351                        }
1352                        break;
1353                    case 'longpress':
1354                        if (this.dragViewport) {
1355                            // If dragViewport is true, we need to wait to see
1356                            // if we have dragged outside the threshold before
1357                            // sending any events to the server.
1358                            this._viewportHasMoved = false;
1359                            this._viewportDragPos = {'x': pos.x, 'y': pos.y};
1360                        } else {
1361                            this._fakeMouseMove(ev, pos.x, pos.y);
1362                            this._handleMouseButton(pos.x, pos.y, 0x4);
1363                        }
1364                        break;
1365                    case 'twodrag':
1366                        this._gestureLastMagnitudeX = ev.detail.magnitudeX;
1367                        this._gestureLastMagnitudeY = ev.detail.magnitudeY;
1368                        this._fakeMouseMove(ev, pos.x, pos.y);
1369                        break;
1370                    case 'pinch':
1371                        this._gestureLastMagnitudeX = Math.hypot(ev.detail.magnitudeX,
1372                                                                 ev.detail.magnitudeY);
1373                        this._fakeMouseMove(ev, pos.x, pos.y);
1374                        break;
1375                }
1376                break;
1377
1378            case 'gesturemove':
1379                switch (ev.detail.type) {
1380                    case 'onetap':
1381                    case 'twotap':
1382                    case 'threetap':
1383                        break;
1384                    case 'drag':
1385                    case 'longpress':
1386                        if (this.dragViewport) {
1387                            this._viewportDragging = true;
1388                            const deltaX = this._viewportDragPos.x - pos.x;
1389                            const deltaY = this._viewportDragPos.y - pos.y;
1390
1391                            if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
1392                                                           Math.abs(deltaY) > dragThreshold)) {
1393                                this._viewportHasMoved = true;
1394
1395                                this._viewportDragPos = {'x': pos.x, 'y': pos.y};
1396                                this._display.viewportChangePos(deltaX, deltaY);
1397                            }
1398                        } else {
1399                            this._fakeMouseMove(ev, pos.x, pos.y);
1400                        }
1401                        break;
1402                    case 'twodrag':
1403                        // Always scroll in the same position.
1404                        // We don't know if the mouse was moved so we need to move it
1405                        // every update.
1406                        this._fakeMouseMove(ev, pos.x, pos.y);
1407                        while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) {
1408                            this._handleMouseButton(pos.x, pos.y, 0x8);
1409                            this._handleMouseButton(pos.x, pos.y, 0x0);
1410                            this._gestureLastMagnitudeY += GESTURE_SCRLSENS;
1411                        }
1412                        while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) < -GESTURE_SCRLSENS) {
1413                            this._handleMouseButton(pos.x, pos.y, 0x10);
1414                            this._handleMouseButton(pos.x, pos.y, 0x0);
1415                            this._gestureLastMagnitudeY -= GESTURE_SCRLSENS;
1416                        }
1417                        while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) > GESTURE_SCRLSENS) {
1418                            this._handleMouseButton(pos.x, pos.y, 0x20);
1419                            this._handleMouseButton(pos.x, pos.y, 0x0);
1420                            this._gestureLastMagnitudeX += GESTURE_SCRLSENS;
1421                        }
1422                        while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) < -GESTURE_SCRLSENS) {
1423                            this._handleMouseButton(pos.x, pos.y, 0x40);
1424                            this._handleMouseButton(pos.x, pos.y, 0x0);
1425                            this._gestureLastMagnitudeX -= GESTURE_SCRLSENS;
1426                        }
1427                        break;
1428                    case 'pinch':
1429                        // Always scroll in the same position.
1430                        // We don't know if the mouse was moved so we need to move it
1431                        // every update.
1432                        this._fakeMouseMove(ev, pos.x, pos.y);
1433                        magnitude = Math.hypot(ev.detail.magnitudeX, ev.detail.magnitudeY);
1434                        if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
1435                            this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
1436                            while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
1437                                this._handleMouseButton(pos.x, pos.y, 0x8);
1438                                this._handleMouseButton(pos.x, pos.y, 0x0);
1439                                this._gestureLastMagnitudeX += GESTURE_ZOOMSENS;
1440                            }
1441                            while ((magnitude -  this._gestureLastMagnitudeX) < -GESTURE_ZOOMSENS) {
1442                                this._handleMouseButton(pos.x, pos.y, 0x10);
1443                                this._handleMouseButton(pos.x, pos.y, 0x0);
1444                                this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS;
1445                            }
1446                        }
1447                        this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", false);
1448                        break;
1449                }
1450                break;
1451
1452            case 'gestureend':
1453                switch (ev.detail.type) {
1454                    case 'onetap':
1455                    case 'twotap':
1456                    case 'threetap':
1457                    case 'pinch':
1458                    case 'twodrag':
1459                        break;
1460                    case 'drag':
1461                        if (this.dragViewport) {
1462                            this._viewportDragging = false;
1463                        } else {
1464                            this._fakeMouseMove(ev, pos.x, pos.y);
1465                            this._handleMouseButton(pos.x, pos.y, 0x0);
1466                        }
1467                        break;
1468                    case 'longpress':
1469                        if (this._viewportHasMoved) {
1470                            // We don't want to send any events if we have moved
1471                            // our viewport
1472                            break;
1473                        }
1474
1475                        if (this.dragViewport && !this._viewportHasMoved) {
1476                            this._fakeMouseMove(ev, pos.x, pos.y);
1477                            // If dragViewport is true, we need to wait to see
1478                            // if we have dragged outside the threshold before
1479                            // sending any events to the server.
1480                            this._handleMouseButton(pos.x, pos.y, 0x4);
1481                            this._handleMouseButton(pos.x, pos.y, 0x0);
1482                            this._viewportDragging = false;
1483                        } else {
1484                            this._fakeMouseMove(ev, pos.x, pos.y);
1485                            this._handleMouseButton(pos.x, pos.y, 0x0);
1486                        }
1487                        break;
1488                }
1489                break;
1490        }
1491    }
1492
1493    _flushMouseMoveTimer(x, y) {
1494        if (this._mouseMoveTimer !== null) {
1495            clearTimeout(this._mouseMoveTimer);
1496            this._mouseMoveTimer = null;
1497            this._sendMouse(x, y, this._mouseButtonMask);
1498        }
1499    }
1500
1501    // Message handlers
1502
1503    _negotiateProtocolVersion() {
1504        if (this._sock.rQwait("version", 12)) {
1505            return false;
1506        }
1507
1508        const sversion = this._sock.rQshiftStr(12).substr(4, 7);
1509        Log.Info("Server ProtocolVersion: " + sversion);
1510        let isRepeater = 0;
1511        switch (sversion) {
1512            case "000.000":  // UltraVNC repeater
1513                isRepeater = 1;
1514                break;
1515            case "003.003":
1516            case "003.006":  // UltraVNC
1517                this._rfbVersion = 3.3;
1518                break;
1519            case "003.007":
1520                this._rfbVersion = 3.7;
1521                break;
1522            case "003.008":
1523            case "003.889":  // Apple Remote Desktop
1524            case "004.000":  // Intel AMT KVM
1525            case "004.001":  // RealVNC 4.6
1526            case "005.000":  // RealVNC 5.3
1527                this._rfbVersion = 3.8;
1528                break;
1529            default:
1530                return this._fail("Invalid server version " + sversion);
1531        }
1532
1533        if (isRepeater) {
1534            let repeaterID = "ID:" + this._repeaterID;
1535            while (repeaterID.length < 250) {
1536                repeaterID += "\0";
1537            }
1538            this._sock.sQpushString(repeaterID);
1539            this._sock.flush();
1540            return true;
1541        }
1542
1543        if (this._rfbVersion > this._rfbMaxVersion) {
1544            this._rfbVersion = this._rfbMaxVersion;
1545        }
1546
1547        const cversion = "00" + parseInt(this._rfbVersion, 10) +
1548                       ".00" + ((this._rfbVersion * 10) % 10);
1549        this._sock.sQpushString("RFB " + cversion + "\n");
1550        this._sock.flush();
1551        Log.Debug('Sent ProtocolVersion: ' + cversion);
1552
1553        this._rfbInitState = 'Security';
1554    }
1555
1556    _isSupportedSecurityType(type) {
1557        const clientTypes = [
1558            securityTypeNone,
1559            securityTypeVNCAuth,
1560            securityTypeRA2ne,
1561            securityTypeTight,
1562            securityTypeVeNCrypt,
1563            securityTypeXVP,
1564            securityTypeARD,
1565            securityTypeMSLogonII,
1566            securityTypePlain,
1567        ];
1568
1569        return clientTypes.includes(type);
1570    }
1571
1572    _negotiateSecurity() {
1573        if (this._rfbVersion >= 3.7) {
1574            // Server sends supported list, client decides
1575            const numTypes = this._sock.rQshift8();
1576            if (this._sock.rQwait("security type", numTypes, 1)) { return false; }
1577
1578            if (numTypes === 0) {
1579                this._rfbInitState = "SecurityReason";
1580                this._securityContext = "no security types";
1581                this._securityStatus = 1;
1582                return true;
1583            }
1584
1585            const types = this._sock.rQshiftBytes(numTypes);
1586            Log.Debug("Server security types: " + types);
1587
1588            // Look for a matching security type in the order that the
1589            // server prefers
1590            this._rfbAuthScheme = -1;
1591            for (let type of types) {
1592                if (this._isSupportedSecurityType(type)) {
1593                    this._rfbAuthScheme = type;
1594                    break;
1595                }
1596            }
1597
1598            if (this._rfbAuthScheme === -1) {
1599                return this._fail("Unsupported security types (types: " + types + ")");
1600            }
1601
1602            this._sock.sQpush8(this._rfbAuthScheme);
1603            this._sock.flush();
1604        } else {
1605            // Server decides
1606            if (this._sock.rQwait("security scheme", 4)) { return false; }
1607            this._rfbAuthScheme = this._sock.rQshift32();
1608
1609            if (this._rfbAuthScheme == 0) {
1610                this._rfbInitState = "SecurityReason";
1611                this._securityContext = "authentication scheme";
1612                this._securityStatus = 1;
1613                return true;
1614            }
1615        }
1616
1617        this._rfbInitState = 'Authentication';
1618        Log.Debug('Authenticating using scheme: ' + this._rfbAuthScheme);
1619
1620        return true;
1621    }
1622
1623    _handleSecurityReason() {
1624        if (this._sock.rQwait("reason length", 4)) {
1625            return false;
1626        }
1627        const strlen = this._sock.rQshift32();
1628        let reason = "";
1629
1630        if (strlen > 0) {
1631            if (this._sock.rQwait("reason", strlen, 4)) { return false; }
1632            reason = this._sock.rQshiftStr(strlen);
1633        }
1634
1635        if (reason !== "") {
1636            this.dispatchEvent(new CustomEvent(
1637                "securityfailure",
1638                { detail: { status: this._securityStatus,
1639                            reason: reason } }));
1640
1641            return this._fail("Security negotiation failed on " +
1642                              this._securityContext +
1643                              " (reason: " + reason + ")");
1644        } else {
1645            this.dispatchEvent(new CustomEvent(
1646                "securityfailure",
1647                { detail: { status: this._securityStatus } }));
1648
1649            return this._fail("Security negotiation failed on " +
1650                              this._securityContext);
1651        }
1652    }
1653
1654    // authentication
1655    _negotiateXvpAuth() {
1656        if (this._rfbCredentials.username === undefined ||
1657            this._rfbCredentials.password === undefined ||
1658            this._rfbCredentials.target === undefined) {
1659            this.dispatchEvent(new CustomEvent(
1660                "credentialsrequired",
1661                { detail: { types: ["username", "password", "target"] } }));
1662            return false;
1663        }
1664
1665        this._sock.sQpush8(this._rfbCredentials.username.length);
1666        this._sock.sQpush8(this._rfbCredentials.target.length);
1667        this._sock.sQpushString(this._rfbCredentials.username);
1668        this._sock.sQpushString(this._rfbCredentials.target);
1669
1670        this._sock.flush();
1671
1672        this._rfbAuthScheme = securityTypeVNCAuth;
1673
1674        return this._negotiateAuthentication();
1675    }
1676
1677    // VeNCrypt authentication, currently only supports version 0.2 and only Plain subtype
1678    _negotiateVeNCryptAuth() {
1679
1680        // waiting for VeNCrypt version
1681        if (this._rfbVeNCryptState == 0) {
1682            if (this._sock.rQwait("vencrypt version", 2)) { return false; }
1683
1684            const major = this._sock.rQshift8();
1685            const minor = this._sock.rQshift8();
1686
1687            if (!(major == 0 && minor == 2)) {
1688                return this._fail("Unsupported VeNCrypt version " + major + "." + minor);
1689            }
1690
1691            this._sock.sQpush8(0);
1692            this._sock.sQpush8(2);
1693            this._sock.flush();
1694            this._rfbVeNCryptState = 1;
1695        }
1696
1697        // waiting for ACK
1698        if (this._rfbVeNCryptState == 1) {
1699            if (this._sock.rQwait("vencrypt ack", 1)) { return false; }
1700
1701            const res = this._sock.rQshift8();
1702
1703            if (res != 0) {
1704                return this._fail("VeNCrypt failure " + res);
1705            }
1706
1707            this._rfbVeNCryptState = 2;
1708        }
1709        // must fall through here (i.e. no "else if"), beacause we may have already received
1710        // the subtypes length and won't be called again
1711
1712        if (this._rfbVeNCryptState == 2) { // waiting for subtypes length
1713            if (this._sock.rQwait("vencrypt subtypes length", 1)) { return false; }
1714
1715            const subtypesLength = this._sock.rQshift8();
1716            if (subtypesLength < 1) {
1717                return this._fail("VeNCrypt subtypes empty");
1718            }
1719
1720            this._rfbVeNCryptSubtypesLength = subtypesLength;
1721            this._rfbVeNCryptState = 3;
1722        }
1723
1724        // waiting for subtypes list
1725        if (this._rfbVeNCryptState == 3) {
1726            if (this._sock.rQwait("vencrypt subtypes", 4 * this._rfbVeNCryptSubtypesLength)) { return false; }
1727
1728            const subtypes = [];
1729            for (let i = 0; i < this._rfbVeNCryptSubtypesLength; i++) {
1730                subtypes.push(this._sock.rQshift32());
1731            }
1732
1733            // Look for a matching security type in the order that the
1734            // server prefers
1735            this._rfbAuthScheme = -1;
1736            for (let type of subtypes) {
1737                // Avoid getting in to a loop
1738                if (type === securityTypeVeNCrypt) {
1739                    continue;
1740                }
1741
1742                if (this._isSupportedSecurityType(type)) {
1743                    this._rfbAuthScheme = type;
1744                    break;
1745                }
1746            }
1747
1748            if (this._rfbAuthScheme === -1) {
1749                return this._fail("Unsupported security types (types: " + subtypes + ")");
1750            }
1751
1752            this._sock.sQpush32(this._rfbAuthScheme);
1753            this._sock.flush();
1754
1755            this._rfbVeNCryptState = 4;
1756            return true;
1757        }
1758    }
1759
1760    _negotiatePlainAuth() {
1761        if (this._rfbCredentials.username === undefined ||
1762            this._rfbCredentials.password === undefined) {
1763            this.dispatchEvent(new CustomEvent(
1764                "credentialsrequired",
1765                { detail: { types: ["username", "password"] } }));
1766            return false;
1767        }
1768
1769        const user = encodeUTF8(this._rfbCredentials.username);
1770        const pass = encodeUTF8(this._rfbCredentials.password);
1771
1772        this._sock.sQpush32(user.length);
1773        this._sock.sQpush32(pass.length);
1774        this._sock.sQpushString(user);
1775        this._sock.sQpushString(pass);
1776        this._sock.flush();
1777
1778        this._rfbInitState = "SecurityResult";
1779        return true;
1780    }
1781
1782    _negotiateStdVNCAuth() {
1783        if (this._sock.rQwait("auth challenge", 16)) { return false; }
1784
1785        if (this._rfbCredentials.password === undefined) {
1786            this.dispatchEvent(new CustomEvent(
1787                "credentialsrequired",
1788                { detail: { types: ["password"] } }));
1789            return false;
1790        }
1791
1792        // TODO(directxman12): make genDES not require an Array
1793        const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16));
1794        const response = RFB.genDES(this._rfbCredentials.password, challenge);
1795        this._sock.sQpushBytes(response);
1796        this._sock.flush();
1797        this._rfbInitState = "SecurityResult";
1798        return true;
1799    }
1800
1801    _negotiateARDAuth() {
1802
1803        if (this._rfbCredentials.username === undefined ||
1804            this._rfbCredentials.password === undefined) {
1805            this.dispatchEvent(new CustomEvent(
1806                "credentialsrequired",
1807                { detail: { types: ["username", "password"] } }));
1808            return false;
1809        }
1810
1811        if (this._rfbCredentials.ardPublicKey != undefined &&
1812            this._rfbCredentials.ardCredentials != undefined) {
1813            // if the async web crypto is done return the results
1814            this._sock.sQpushBytes(this._rfbCredentials.ardCredentials);
1815            this._sock.sQpushBytes(this._rfbCredentials.ardPublicKey);
1816            this._sock.flush();
1817            this._rfbCredentials.ardCredentials = null;
1818            this._rfbCredentials.ardPublicKey = null;
1819            this._rfbInitState = "SecurityResult";
1820            return true;
1821        }
1822
1823        if (this._sock.rQwait("read ard", 4)) { return false; }
1824
1825        let generator = this._sock.rQshiftBytes(2);   // DH base generator value
1826
1827        let keyLength = this._sock.rQshift16();
1828
1829        if (this._sock.rQwait("read ard keylength", keyLength*2, 4)) { return false; }
1830
1831        // read the server values
1832        let prime = this._sock.rQshiftBytes(keyLength);  // predetermined prime modulus
1833        let serverPublicKey = this._sock.rQshiftBytes(keyLength); // other party's public key
1834
1835        let clientKey = legacyCrypto.generateKey(
1836            { name: "DH", g: generator, p: prime }, false, ["deriveBits"]);
1837        this._negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey);
1838
1839        return false;
1840    }
1841
1842    async _negotiateARDAuthAsync(keyLength, serverPublicKey, clientKey) {
1843        const clientPublicKey = legacyCrypto.exportKey("raw", clientKey.publicKey);
1844        const sharedKey = legacyCrypto.deriveBits(
1845            { name: "DH", public: serverPublicKey }, clientKey.privateKey, keyLength * 8);
1846
1847        const username = encodeUTF8(this._rfbCredentials.username).substring(0, 63);
1848        const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
1849
1850        const credentials = window.crypto.getRandomValues(new Uint8Array(128));
1851        for (let i = 0; i < username.length; i++) {
1852            credentials[i] = username.charCodeAt(i);
1853        }
1854        credentials[username.length] = 0;
1855        for (let i = 0; i < password.length; i++) {
1856            credentials[64 + i] = password.charCodeAt(i);
1857        }
1858        credentials[64 + password.length] = 0;
1859
1860        const key = await legacyCrypto.digest("MD5", sharedKey);
1861        const cipher = await legacyCrypto.importKey(
1862            "raw", key, { name: "AES-ECB" }, false, ["encrypt"]);
1863        const encrypted = await legacyCrypto.encrypt({ name: "AES-ECB" }, cipher, credentials);
1864
1865        this._rfbCredentials.ardCredentials = encrypted;
1866        this._rfbCredentials.ardPublicKey = clientPublicKey;
1867
1868        this._resumeAuthentication();
1869    }
1870
1871    _negotiateTightUnixAuth() {
1872        if (this._rfbCredentials.username === undefined ||
1873            this._rfbCredentials.password === undefined) {
1874            this.dispatchEvent(new CustomEvent(
1875                "credentialsrequired",
1876                { detail: { types: ["username", "password"] } }));
1877            return false;
1878        }
1879
1880        this._sock.sQpush32(this._rfbCredentials.username.length);
1881        this._sock.sQpush32(this._rfbCredentials.password.length);
1882        this._sock.sQpushString(this._rfbCredentials.username);
1883        this._sock.sQpushString(this._rfbCredentials.password);
1884        this._sock.flush();
1885
1886        this._rfbInitState = "SecurityResult";
1887        return true;
1888    }
1889
1890    _negotiateTightTunnels(numTunnels) {
1891        const clientSupportedTunnelTypes = {
1892            0: { vendor: 'TGHT', signature: 'NOTUNNEL' }
1893        };
1894        const serverSupportedTunnelTypes = {};
1895        // receive tunnel capabilities
1896        for (let i = 0; i < numTunnels; i++) {
1897            const capCode = this._sock.rQshift32();
1898            const capVendor = this._sock.rQshiftStr(4);
1899            const capSignature = this._sock.rQshiftStr(8);
1900            serverSupportedTunnelTypes[capCode] = { vendor: capVendor, signature: capSignature };
1901        }
1902
1903        Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes);
1904
1905        // Siemens touch panels have a VNC server that supports NOTUNNEL,
1906        // but forgets to advertise it. Try to detect such servers by
1907        // looking for their custom tunnel type.
1908        if (serverSupportedTunnelTypes[1] &&
1909            (serverSupportedTunnelTypes[1].vendor === "SICR") &&
1910            (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) {
1911            Log.Debug("Detected Siemens server. Assuming NOTUNNEL support.");
1912            serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' };
1913        }
1914
1915        // choose the notunnel type
1916        if (serverSupportedTunnelTypes[0]) {
1917            if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
1918                serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
1919                return this._fail("Client's tunnel type had the incorrect " +
1920                                  "vendor or signature");
1921            }
1922            Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]);
1923            this._sock.sQpush32(0); // use NOTUNNEL
1924            this._sock.flush();
1925            return false; // wait until we receive the sub auth count to continue
1926        } else {
1927            return this._fail("Server wanted tunnels, but doesn't support " +
1928                              "the notunnel type");
1929        }
1930    }
1931
1932    _negotiateTightAuth() {
1933        if (!this._rfbTightVNC) {  // first pass, do the tunnel negotiation
1934            if (this._sock.rQwait("num tunnels", 4)) { return false; }
1935            const numTunnels = this._sock.rQshift32();
1936            if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; }
1937
1938            this._rfbTightVNC = true;
1939
1940            if (numTunnels > 0) {
1941                this._negotiateTightTunnels(numTunnels);
1942                return false;  // wait until we receive the sub auth to continue
1943            }
1944        }
1945
1946        // second pass, do the sub-auth negotiation
1947        if (this._sock.rQwait("sub auth count", 4)) { return false; }
1948        const subAuthCount = this._sock.rQshift32();
1949        if (subAuthCount === 0) {  // empty sub-auth list received means 'no auth' subtype selected
1950            this._rfbInitState = 'SecurityResult';
1951            return true;
1952        }
1953
1954        if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; }
1955
1956        const clientSupportedTypes = {
1957            'STDVNOAUTH__': 1,
1958            'STDVVNCAUTH_': 2,
1959            'TGHTULGNAUTH': 129
1960        };
1961
1962        const serverSupportedTypes = [];
1963
1964        for (let i = 0; i < subAuthCount; i++) {
1965            this._sock.rQshift32(); // capNum
1966            const capabilities = this._sock.rQshiftStr(12);
1967            serverSupportedTypes.push(capabilities);
1968        }
1969
1970        Log.Debug("Server Tight authentication types: " + serverSupportedTypes);
1971
1972        for (let authType in clientSupportedTypes) {
1973            if (serverSupportedTypes.indexOf(authType) != -1) {
1974                this._sock.sQpush32(clientSupportedTypes[authType]);
1975                this._sock.flush();
1976                Log.Debug("Selected authentication type: " + authType);
1977
1978                switch (authType) {
1979                    case 'STDVNOAUTH__':  // no auth
1980                        this._rfbInitState = 'SecurityResult';
1981                        return true;
1982                    case 'STDVVNCAUTH_':
1983                        this._rfbAuthScheme = securityTypeVNCAuth;
1984                        return true;
1985                    case 'TGHTULGNAUTH':
1986                        this._rfbAuthScheme = securityTypeUnixLogon;
1987                        return true;
1988                    default:
1989                        return this._fail("Unsupported tiny auth scheme " +
1990                                          "(scheme: " + authType + ")");
1991                }
1992            }
1993        }
1994
1995        return this._fail("No supported sub-auth types!");
1996    }
1997
1998    _handleRSAAESCredentialsRequired(event) {
1999        this.dispatchEvent(event);
2000    }
2001
2002    _handleRSAAESServerVerification(event) {
2003        this.dispatchEvent(event);
2004    }
2005
2006    _negotiateRA2neAuth() {
2007        if (this._rfbRSAAESAuthenticationState === null) {
2008            this._rfbRSAAESAuthenticationState = new RSAAESAuthenticationState(this._sock, () => this._rfbCredentials);
2009            this._rfbRSAAESAuthenticationState.addEventListener(
2010                "serververification", this._eventHandlers.handleRSAAESServerVerification);
2011            this._rfbRSAAESAuthenticationState.addEventListener(
2012                "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
2013        }
2014        this._rfbRSAAESAuthenticationState.checkInternalEvents();
2015        if (!this._rfbRSAAESAuthenticationState.hasStarted) {
2016            this._rfbRSAAESAuthenticationState.negotiateRA2neAuthAsync()
2017                .catch((e) => {
2018                    if (e.message !== "disconnect normally") {
2019                        this._fail(e.message);
2020                    }
2021                })
2022                .then(() => {
2023                    this._rfbInitState = "SecurityResult";
2024                    return true;
2025                }).finally(() => {
2026                    this._rfbRSAAESAuthenticationState.removeEventListener(
2027                        "serververification", this._eventHandlers.handleRSAAESServerVerification);
2028                    this._rfbRSAAESAuthenticationState.removeEventListener(
2029                        "credentialsrequired", this._eventHandlers.handleRSAAESCredentialsRequired);
2030                    this._rfbRSAAESAuthenticationState = null;
2031                });
2032        }
2033        return false;
2034    }
2035
2036    _negotiateMSLogonIIAuth() {
2037        if (this._sock.rQwait("mslogonii dh param", 24)) { return false; }
2038
2039        if (this._rfbCredentials.username === undefined ||
2040            this._rfbCredentials.password === undefined) {
2041            this.dispatchEvent(new CustomEvent(
2042                "credentialsrequired",
2043                { detail: { types: ["username", "password"] } }));
2044            return false;
2045        }
2046
2047        const g = this._sock.rQshiftBytes(8);
2048        const p = this._sock.rQshiftBytes(8);
2049        const A = this._sock.rQshiftBytes(8);
2050        const dhKey = legacyCrypto.generateKey({ name: "DH", g: g, p: p }, true, ["deriveBits"]);
2051        const B = legacyCrypto.exportKey("raw", dhKey.publicKey);
2052        const secret = legacyCrypto.deriveBits({ name: "DH", public: A }, dhKey.privateKey, 64);
2053
2054        const key = legacyCrypto.importKey("raw", secret, { name: "DES-CBC" }, false, ["encrypt"]);
2055        const username = encodeUTF8(this._rfbCredentials.username).substring(0, 255);
2056        const password = encodeUTF8(this._rfbCredentials.password).substring(0, 63);
2057        let usernameBytes = new Uint8Array(256);
2058        let passwordBytes = new Uint8Array(64);
2059        window.crypto.getRandomValues(usernameBytes);
2060        window.crypto.getRandomValues(passwordBytes);
2061        for (let i = 0; i < username.length; i++) {
2062            usernameBytes[i] = username.charCodeAt(i);
2063        }
2064        usernameBytes[username.length] = 0;
2065        for (let i = 0; i < password.length; i++) {
2066            passwordBytes[i] = password.charCodeAt(i);
2067        }
2068        passwordBytes[password.length] = 0;
2069        usernameBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, usernameBytes);
2070        passwordBytes = legacyCrypto.encrypt({ name: "DES-CBC", iv: secret }, key, passwordBytes);
2071        this._sock.sQpushBytes(B);
2072        this._sock.sQpushBytes(usernameBytes);
2073        this._sock.sQpushBytes(passwordBytes);
2074        this._sock.flush();
2075        this._rfbInitState = "SecurityResult";
2076        return true;
2077    }
2078
2079    _negotiateAuthentication() {
2080        switch (this._rfbAuthScheme) {
2081            case securityTypeNone:
2082                if (this._rfbVersion >= 3.8) {
2083                    this._rfbInitState = 'SecurityResult';
2084                } else {
2085                    this._rfbInitState = 'ClientInitialisation';
2086                }
2087                return true;
2088
2089            case securityTypeXVP:
2090                return this._negotiateXvpAuth();
2091
2092            case securityTypeARD:
2093                return this._negotiateARDAuth();
2094
2095            case securityTypeVNCAuth:
2096                return this._negotiateStdVNCAuth();
2097
2098            case securityTypeTight:
2099                return this._negotiateTightAuth();
2100
2101            case securityTypeVeNCrypt:
2102                return this._negotiateVeNCryptAuth();
2103
2104            case securityTypePlain:
2105                return this._negotiatePlainAuth();
2106
2107            case securityTypeUnixLogon:
2108                return this._negotiateTightUnixAuth();
2109
2110            case securityTypeRA2ne:
2111                return this._negotiateRA2neAuth();
2112
2113            case securityTypeMSLogonII:
2114                return this._negotiateMSLogonIIAuth();
2115
2116            default:
2117                return this._fail("Unsupported auth scheme (scheme: " +
2118                                  this._rfbAuthScheme + ")");
2119        }
2120    }
2121
2122    _handleSecurityResult() {
2123        if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
2124
2125        const status = this._sock.rQshift32();
2126
2127        if (status === 0) { // OK
2128            this._rfbInitState = 'ClientInitialisation';
2129            Log.Debug('Authentication OK');
2130            return true;
2131        } else {
2132            if (this._rfbVersion >= 3.8) {
2133                this._rfbInitState = "SecurityReason";
2134                this._securityContext = "security result";
2135                this._securityStatus = status;
2136                return true;
2137            } else {
2138                this.dispatchEvent(new CustomEvent(
2139                    "securityfailure",
2140                    { detail: { status: status } }));
2141
2142                return this._fail("Security handshake failed");
2143            }
2144        }
2145    }
2146
2147    _negotiateServerInit() {
2148        if (this._sock.rQwait("server initialization", 24)) { return false; }
2149
2150        /* Screen size */
2151        const width = this._sock.rQshift16();
2152        const height = this._sock.rQshift16();
2153
2154        /* PIXEL_FORMAT */
2155        const bpp         = this._sock.rQshift8();
2156        const depth       = this._sock.rQshift8();
2157        const bigEndian  = this._sock.rQshift8();
2158        const trueColor  = this._sock.rQshift8();
2159
2160        const redMax     = this._sock.rQshift16();
2161        const greenMax   = this._sock.rQshift16();
2162        const blueMax    = this._sock.rQshift16();
2163        const redShift   = this._sock.rQshift8();
2164        const greenShift = this._sock.rQshift8();
2165        const blueShift  = this._sock.rQshift8();
2166        this._sock.rQskipBytes(3);  // padding
2167
2168        // NB(directxman12): we don't want to call any callbacks or print messages until
2169        //                   *after* we're past the point where we could backtrack
2170
2171        /* Connection name/title */
2172        const nameLength = this._sock.rQshift32();
2173        if (this._sock.rQwait('server init name', nameLength, 24)) { return false; }
2174        let name = this._sock.rQshiftStr(nameLength);
2175        name = decodeUTF8(name, true);
2176
2177        if (this._rfbTightVNC) {
2178            if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + nameLength)) { return false; }
2179            // In TightVNC mode, ServerInit message is extended
2180            const numServerMessages = this._sock.rQshift16();
2181            const numClientMessages = this._sock.rQshift16();
2182            const numEncodings = this._sock.rQshift16();
2183            this._sock.rQskipBytes(2);  // padding
2184
2185            const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16;
2186            if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + nameLength)) { return false; }
2187
2188            // we don't actually do anything with the capability information that TIGHT sends,
2189            // so we just skip the all of this.
2190
2191            // TIGHT server message capabilities
2192            this._sock.rQskipBytes(16 * numServerMessages);
2193
2194            // TIGHT client message capabilities
2195            this._sock.rQskipBytes(16 * numClientMessages);
2196
2197            // TIGHT encoding capabilities
2198            this._sock.rQskipBytes(16 * numEncodings);
2199        }
2200
2201        // NB(directxman12): these are down here so that we don't run them multiple times
2202        //                   if we backtrack
2203        Log.Info("Screen: " + width + "x" + height +
2204                  ", bpp: " + bpp + ", depth: " + depth +
2205                  ", bigEndian: " + bigEndian +
2206                  ", trueColor: " + trueColor +
2207                  ", redMax: " + redMax +
2208                  ", greenMax: " + greenMax +
2209                  ", blueMax: " + blueMax +
2210                  ", redShift: " + redShift +
2211                  ", greenShift: " + greenShift +
2212                  ", blueShift: " + blueShift);
2213
2214        // we're past the point where we could backtrack, so it's safe to call this
2215        this._setDesktopName(name);
2216        this._resize(width, height);
2217
2218        if (!this._viewOnly) {
2219            this._keyboard.grab();
2220            this._asyncClipboard.grab();
2221        }
2222
2223        this._fbDepth = 24;
2224
2225        if (this._fbName === "Intel(r) AMT KVM") {
2226            Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode.");
2227            this._fbDepth = 8;
2228        }
2229
2230        RFB.messages.pixelFormat(this._sock, this._fbDepth, true);
2231        this._sendEncodings();
2232        RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fbWidth, this._fbHeight);
2233
2234        this._updateConnectionState('connected');
2235        return true;
2236    }
2237
2238    _sendEncodings() {
2239        const encs = [];
2240
2241        // In preference order
2242        encs.push(encodings.encodingCopyRect);
2243        // Only supported with full depth support
2244        if (this._fbDepth == 24) {
2245            if (supportsWebCodecsH264Decode) {
2246                encs.push(encodings.encodingH264);
2247            }
2248            encs.push(encodings.encodingTight);
2249            encs.push(encodings.encodingTightPNG);
2250            encs.push(encodings.encodingZRLE);
2251            encs.push(encodings.encodingJPEG);
2252            encs.push(encodings.encodingHextile);
2253            encs.push(encodings.encodingRRE);
2254            encs.push(encodings.encodingZlib);
2255        }
2256        encs.push(encodings.encodingRaw);
2257
2258        // Psuedo-encoding settings
2259        encs.push(encodings.pseudoEncodingQualityLevel0 + this._qualityLevel);
2260        encs.push(encodings.pseudoEncodingCompressLevel0 + this._compressionLevel);
2261
2262        encs.push(encodings.pseudoEncodingDesktopSize);
2263        encs.push(encodings.pseudoEncodingLastRect);
2264        encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent);
2265        encs.push(encodings.pseudoEncodingQEMULedEvent);
2266        encs.push(encodings.pseudoEncodingExtendedDesktopSize);
2267        encs.push(encodings.pseudoEncodingXvp);
2268        encs.push(encodings.pseudoEncodingFence);
2269        encs.push(encodings.pseudoEncodingContinuousUpdates);
2270        encs.push(encodings.pseudoEncodingDesktopName);
2271        encs.push(encodings.pseudoEncodingExtendedClipboard);
2272        encs.push(encodings.pseudoEncodingExtendedMouseButtons);
2273
2274        if (this._fbDepth == 24) {
2275            encs.push(encodings.pseudoEncodingVMwareCursor);
2276            encs.push(encodings.pseudoEncodingCursor);
2277        }
2278
2279        RFB.messages.clientEncodings(this._sock, encs);
2280    }
2281
2282    /* RFB protocol initialization states:
2283     *   ProtocolVersion
2284     *   Security
2285     *   Authentication
2286     *   SecurityResult
2287     *   ClientInitialization - not triggered by server message
2288     *   ServerInitialization
2289     */
2290    _initMsg() {
2291        switch (this._rfbInitState) {
2292            case 'ProtocolVersion':
2293                return this._negotiateProtocolVersion();
2294
2295            case 'Security':
2296                return this._negotiateSecurity();
2297
2298            case 'Authentication':
2299                return this._negotiateAuthentication();
2300
2301            case 'SecurityResult':
2302                return this._handleSecurityResult();
2303
2304            case 'SecurityReason':
2305                return this._handleSecurityReason();
2306
2307            case 'ClientInitialisation':
2308                this._sock.sQpush8(this._shared ? 1 : 0); // ClientInitialisation
2309                this._sock.flush();
2310                this._rfbInitState = 'ServerInitialisation';
2311                return true;
2312
2313            case 'ServerInitialisation':
2314                return this._negotiateServerInit();
2315
2316            default:
2317                return this._fail("Unknown init state (state: " +
2318                                  this._rfbInitState + ")");
2319        }
2320    }
2321
2322    // Resume authentication handshake after it was paused for some
2323    // reason, e.g. waiting for a password from the user
2324    _resumeAuthentication() {
2325        // We use setTimeout() so it's run in its own context, just like
2326        // it originally did via the WebSocket's event handler
2327        setTimeout(this._initMsg.bind(this), 0);
2328    }
2329
2330    _handleSetColourMapMsg() {
2331        Log.Debug("SetColorMapEntries");
2332
2333        return this._fail("Unexpected SetColorMapEntries message");
2334    }
2335
2336    _writeClipboard(text) {
2337        if (this._viewOnly) return;
2338        if (this._asyncClipboard.writeClipboard(text)) return;
2339        // Fallback clipboard
2340        this.dispatchEvent(
2341            new CustomEvent("clipboard", {detail: {text: text}})
2342        );
2343    }
2344
2345    _handleServerCutText() {
2346        Log.Debug("ServerCutText");
2347
2348        if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; }
2349
2350        this._sock.rQskipBytes(3);  // Padding
2351
2352        let length = this._sock.rQshift32();
2353        length = toSigned32bit(length);
2354
2355        if (this._sock.rQwait("ServerCutText content", Math.abs(length), 8)) { return false; }
2356
2357        if (length >= 0) {
2358            //Standard msg
2359            const text = this._sock.rQshiftStr(length);
2360            if (this._viewOnly) {
2361                return true;
2362            }
2363
2364            this._writeClipboard(text);
2365
2366        } else {
2367            //Extended msg.
2368            length = Math.abs(length);
2369            const flags = this._sock.rQshift32();
2370            let formats = flags & 0x0000FFFF;
2371            let actions = flags & 0xFF000000;
2372
2373            let isCaps = (!!(actions & extendedClipboardActionCaps));
2374            if (isCaps) {
2375                this._clipboardServerCapabilitiesFormats = {};
2376                this._clipboardServerCapabilitiesActions = {};
2377
2378                // Update our server capabilities for Formats
2379                for (let i = 0; i <= 15; i++) {
2380                    let index = 1 << i;
2381
2382                    // Check if format flag is set.
2383                    if ((formats & index)) {
2384                        this._clipboardServerCapabilitiesFormats[index] = true;
2385                        // We don't send unsolicited clipboard, so we
2386                        // ignore the size
2387                        this._sock.rQshift32();
2388                    }
2389                }
2390
2391                // Update our server capabilities for Actions
2392                for (let i = 24; i <= 31; i++) {
2393                    let index = 1 << i;
2394                    this._clipboardServerCapabilitiesActions[index] = !!(actions & index);
2395                }
2396
2397                /*  Caps handling done, send caps with the clients
2398                    capabilities set as a response */
2399                let clientActions = [
2400                    extendedClipboardActionCaps,
2401                    extendedClipboardActionRequest,
2402                    extendedClipboardActionPeek,
2403                    extendedClipboardActionNotify,
2404                    extendedClipboardActionProvide
2405                ];
2406                RFB.messages.extendedClipboardCaps(this._sock, clientActions, {extendedClipboardFormatText: 0});
2407
2408            } else if (actions === extendedClipboardActionRequest) {
2409                if (this._viewOnly) {
2410                    return true;
2411                }
2412
2413                // Check if server has told us it can handle Provide and there is clipboard data to send.
2414                if (this._clipboardText != null &&
2415                    this._clipboardServerCapabilitiesActions[extendedClipboardActionProvide]) {
2416
2417                    if (formats & extendedClipboardFormatText) {
2418                        RFB.messages.extendedClipboardProvide(this._sock, [extendedClipboardFormatText], [this._clipboardText]);
2419                    }
2420                }
2421
2422            } else if (actions === extendedClipboardActionPeek) {
2423                if (this._viewOnly) {
2424                    return true;
2425                }
2426
2427                if (this._clipboardServerCapabilitiesActions[extendedClipboardActionNotify]) {
2428
2429                    if (this._clipboardText != null) {
2430                        RFB.messages.extendedClipboardNotify(this._sock, [extendedClipboardFormatText]);
2431                    } else {
2432                        RFB.messages.extendedClipboardNotify(this._sock, []);
2433                    }
2434                }
2435
2436            } else if (actions === extendedClipboardActionNotify) {
2437                if (this._viewOnly) {
2438                    return true;
2439                }
2440
2441                if (this._clipboardServerCapabilitiesActions[extendedClipboardActionRequest]) {
2442
2443                    if (formats & extendedClipboardFormatText) {
2444                        RFB.messages.extendedClipboardRequest(this._sock, [extendedClipboardFormatText]);
2445                    }
2446                }
2447
2448            } else if (actions === extendedClipboardActionProvide) {
2449                if (this._viewOnly) {
2450                    return true;
2451                }
2452
2453                if (!(formats & extendedClipboardFormatText)) {
2454                    return true;
2455                }
2456                // Ignore what we had in our clipboard client side.
2457                this._clipboardText = null;
2458
2459                // FIXME: Should probably verify that this data was actually requested
2460                let zlibStream = this._sock.rQshiftBytes(length - 4);
2461                let streamInflator = new Inflator();
2462                let textData = null;
2463
2464                streamInflator.setInput(zlibStream);
2465                for (let i = 0; i <= 15; i++) {
2466                    let format = 1 << i;
2467
2468                    if (formats & format) {
2469
2470                        let size = 0x00;
2471                        let sizeArray = streamInflator.inflate(4);
2472
2473                        size |= (sizeArray[0] << 24);
2474                        size |= (sizeArray[1] << 16);
2475                        size |= (sizeArray[2] << 8);
2476                        size |= (sizeArray[3]);
2477                        let chunk = streamInflator.inflate(size);
2478
2479                        if (format === extendedClipboardFormatText) {
2480                            textData = chunk;
2481                        }
2482                    }
2483                }
2484                streamInflator.setInput(null);
2485
2486                if (textData !== null) {
2487                    let tmpText = "";
2488                    for (let i = 0; i < textData.length; i++) {
2489                        tmpText += String.fromCharCode(textData[i]);
2490                    }
2491                    textData = tmpText;
2492
2493                    textData = decodeUTF8(textData);
2494                    if ((textData.length > 0) && "\0" === textData.charAt(textData.length - 1)) {
2495                        textData = textData.slice(0, -1);
2496                    }
2497
2498                    textData = textData.replaceAll("\r\n", "\n");
2499
2500                    this._writeClipboard(textData);
2501                }
2502            } else {
2503                return this._fail("Unexpected action in extended clipboard message: " + actions);
2504            }
2505        }
2506        return true;
2507    }
2508
2509    _handleServerFenceMsg() {
2510        if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; }
2511        this._sock.rQskipBytes(3); // Padding
2512        let flags = this._sock.rQshift32();
2513        let length = this._sock.rQshift8();
2514
2515        if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; }
2516
2517        if (length > 64) {
2518            Log.Warn("Bad payload length (" + length + ") in fence response");
2519            length = 64;
2520        }
2521
2522        const payload = this._sock.rQshiftStr(length);
2523
2524        this._supportsFence = true;
2525
2526        /*
2527         * Fence flags
2528         *
2529         *  (1<<0)  - BlockBefore
2530         *  (1<<1)  - BlockAfter
2531         *  (1<<2)  - SyncNext
2532         *  (1<<31) - Request
2533         */
2534
2535        if (!(flags & (1<<31))) {
2536            return this._fail("Unexpected fence response");
2537        }
2538
2539        // Filter out unsupported flags
2540        // FIXME: support syncNext
2541        flags &= (1<<0) | (1<<1);
2542
2543        // BlockBefore and BlockAfter are automatically handled by
2544        // the fact that we process each incoming message
2545        // synchronuosly.
2546        RFB.messages.clientFence(this._sock, flags, payload);
2547
2548        return true;
2549    }
2550
2551    _handleXvpMsg() {
2552        if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; }
2553        this._sock.rQskipBytes(1);  // Padding
2554        const xvpVer = this._sock.rQshift8();
2555        const xvpMsg = this._sock.rQshift8();
2556
2557        switch (xvpMsg) {
2558            case 0:  // XVP_FAIL
2559                Log.Error("XVP operation failed");
2560                break;
2561            case 1:  // XVP_INIT
2562                this._rfbXvpVer = xvpVer;
2563                Log.Info("XVP extensions enabled (version " + this._rfbXvpVer + ")");
2564                this._setCapability("power", true);
2565                break;
2566            default:
2567                this._fail("Illegal server XVP message (msg: " + xvpMsg + ")");
2568                break;
2569        }
2570
2571        return true;
2572    }
2573
2574    _normalMsg() {
2575        let msgType;
2576        if (this._FBU.rects > 0) {
2577            msgType = 0;
2578        } else {
2579            msgType = this._sock.rQshift8();
2580        }
2581
2582        let first, ret;
2583        switch (msgType) {
2584            case 0:  // FramebufferUpdate
2585                ret = this._framebufferUpdate();
2586                if (ret && !this._enabledContinuousUpdates) {
2587                    RFB.messages.fbUpdateRequest(this._sock, true, 0, 0,
2588                                                 this._fbWidth, this._fbHeight);
2589                }
2590                return ret;
2591
2592            case 1:  // SetColorMapEntries
2593                return this._handleSetColourMapMsg();
2594
2595            case 2:  // Bell
2596                Log.Debug("Bell");
2597                this.dispatchEvent(new CustomEvent(
2598                    "bell",
2599                    { detail: {} }));
2600                return true;
2601
2602            case 3:  // ServerCutText
2603                return this._handleServerCutText();
2604
2605            case 150: // EndOfContinuousUpdates
2606                first = !this._supportsContinuousUpdates;
2607                this._supportsContinuousUpdates = true;
2608                this._enabledContinuousUpdates = false;
2609                if (first) {
2610                    this._enabledContinuousUpdates = true;
2611                    this._updateContinuousUpdates();
2612                    Log.Info("Enabling continuous updates.");
2613                } else {
2614                    // FIXME: We need to send a framebufferupdaterequest here
2615                    // if we add support for turning off continuous updates
2616                }
2617                return true;
2618
2619            case 248: // ServerFence
2620                return this._handleServerFenceMsg();
2621
2622            case 250:  // XVP
2623                return this._handleXvpMsg();
2624
2625            default:
2626                this._fail("Unexpected server message (type " + msgType + ")");
2627                Log.Debug("sock.rQpeekBytes(30): " + this._sock.rQpeekBytes(30));
2628                return true;
2629        }
2630    }
2631
2632    _framebufferUpdate() {
2633        if (this._FBU.rects === 0) {
2634            if (this._sock.rQwait("FBU header", 3, 1)) { return false; }
2635            this._sock.rQskipBytes(1);  // Padding
2636            this._FBU.rects = this._sock.rQshift16();
2637
2638            // Make sure the previous frame is fully rendered first
2639            // to avoid building up an excessive queue
2640            if (this._display.pending()) {
2641                this._flushing = true;
2642                this._display.flush()
2643                    .then(() => {
2644                        this._flushing = false;
2645                        // Resume processing
2646                        if (!this._sock.rQwait("message", 1)) {
2647                            this._handleMessage();
2648                        }
2649                    });
2650                return false;
2651            }
2652        }
2653
2654        while (this._FBU.rects > 0) {
2655            if (this._FBU.encoding === null) {
2656                if (this._sock.rQwait("rect header", 12)) { return false; }
2657                /* New FramebufferUpdate */
2658
2659                this._FBU.x = this._sock.rQshift16();
2660                this._FBU.y = this._sock.rQshift16();
2661                this._FBU.width = this._sock.rQshift16();
2662                this._FBU.height = this._sock.rQshift16();
2663                this._FBU.encoding = this._sock.rQshift32();
2664                /* Encodings are signed */
2665                this._FBU.encoding >>= 0;
2666            }
2667
2668            if (!this._handleRect()) {
2669                return false;
2670            }
2671
2672            this._FBU.rects--;
2673            this._FBU.encoding = null;
2674        }
2675
2676        this._display.flip();
2677
2678        return true;  // We finished this FBU
2679    }
2680
2681    _handleRect() {
2682        switch (this._FBU.encoding) {
2683            case encodings.pseudoEncodingLastRect:
2684                this._FBU.rects = 1; // Will be decreased when we return
2685                return true;
2686
2687            case encodings.pseudoEncodingVMwareCursor:
2688                return this._handleVMwareCursor();
2689
2690            case encodings.pseudoEncodingCursor:
2691                return this._handleCursor();
2692
2693            case encodings.pseudoEncodingQEMUExtendedKeyEvent:
2694                this._qemuExtKeyEventSupported = true;
2695                return true;
2696
2697            case encodings.pseudoEncodingDesktopName:
2698                return this._handleDesktopName();
2699
2700            case encodings.pseudoEncodingDesktopSize:
2701                this._resize(this._FBU.width, this._FBU.height);
2702                return true;
2703
2704            case encodings.pseudoEncodingExtendedDesktopSize:
2705                return this._handleExtendedDesktopSize();
2706
2707            case encodings.pseudoEncodingExtendedMouseButtons:
2708                this._extendedPointerEventSupported = true;
2709                return true;
2710
2711            case encodings.pseudoEncodingQEMULedEvent:
2712                return this._handleLedEvent();
2713
2714            default:
2715                return this._handleDataRect();
2716        }
2717    }
2718
2719    _handleVMwareCursor() {
2720        const hotx = this._FBU.x;  // hotspot-x
2721        const hoty = this._FBU.y;  // hotspot-y
2722        const w = this._FBU.width;
2723        const h = this._FBU.height;
2724        if (this._sock.rQwait("VMware cursor encoding", 1)) {
2725            return false;
2726        }
2727
2728        const cursorType = this._sock.rQshift8();
2729
2730        this._sock.rQshift8(); //Padding
2731
2732        let rgba;
2733        const bytesPerPixel = 4;
2734
2735        //Classic cursor
2736        if (cursorType == 0) {
2737            //Used to filter away unimportant bits.
2738            //OR is used for correct conversion in js.
2739            const PIXEL_MASK = 0xffffff00 | 0;
2740            rgba = new Array(w * h * bytesPerPixel);
2741
2742            if (this._sock.rQwait("VMware cursor classic encoding",
2743                                  (w * h * bytesPerPixel) * 2, 2)) {
2744                return false;
2745            }
2746
2747            let andMask = new Array(w * h);
2748            for (let pixel = 0; pixel < (w * h); pixel++) {
2749                andMask[pixel] = this._sock.rQshift32();
2750            }
2751
2752            let xorMask = new Array(w * h);
2753            for (let pixel = 0; pixel < (w * h); pixel++) {
2754                xorMask[pixel] = this._sock.rQshift32();
2755            }
2756
2757            for (let pixel = 0; pixel < (w * h); pixel++) {
2758                if (andMask[pixel] == 0) {
2759                    //Fully opaque pixel
2760                    let bgr = xorMask[pixel];
2761                    let r   = bgr >> 8  & 0xff;
2762                    let g   = bgr >> 16 & 0xff;
2763                    let b   = bgr >> 24 & 0xff;
2764
2765                    rgba[(pixel * bytesPerPixel)     ] = r;    //r
2766                    rgba[(pixel * bytesPerPixel) + 1 ] = g;    //g
2767                    rgba[(pixel * bytesPerPixel) + 2 ] = b;    //b
2768                    rgba[(pixel * bytesPerPixel) + 3 ] = 0xff; //a
2769
2770                } else if ((andMask[pixel] & PIXEL_MASK) ==
2771                           PIXEL_MASK) {
2772                    //Only screen value matters, no mouse colouring
2773                    if (xorMask[pixel] == 0) {
2774                        //Transparent pixel
2775                        rgba[(pixel * bytesPerPixel)     ] = 0x00;
2776                        rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2777                        rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2778                        rgba[(pixel * bytesPerPixel) + 3 ] = 0x00;
2779
2780                    } else if ((xorMask[pixel] & PIXEL_MASK) ==
2781                               PIXEL_MASK) {
2782                        //Inverted pixel, not supported in browsers.
2783                        //Fully opaque instead.
2784                        rgba[(pixel * bytesPerPixel)     ] = 0x00;
2785                        rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2786                        rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2787                        rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
2788
2789                    } else {
2790                        //Unhandled xorMask
2791                        rgba[(pixel * bytesPerPixel)     ] = 0x00;
2792                        rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2793                        rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2794                        rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
2795                    }
2796
2797                } else {
2798                    //Unhandled andMask
2799                    rgba[(pixel * bytesPerPixel)     ] = 0x00;
2800                    rgba[(pixel * bytesPerPixel) + 1 ] = 0x00;
2801                    rgba[(pixel * bytesPerPixel) + 2 ] = 0x00;
2802                    rgba[(pixel * bytesPerPixel) + 3 ] = 0xff;
2803                }
2804            }
2805
2806        //Alpha cursor.
2807        } else if (cursorType == 1) {
2808            if (this._sock.rQwait("VMware cursor alpha encoding",
2809                                  (w * h * 4), 2)) {
2810                return false;
2811            }
2812
2813            rgba = new Array(w * h * bytesPerPixel);
2814
2815            for (let pixel = 0; pixel < (w * h); pixel++) {
2816                let data = this._sock.rQshift32();
2817
2818                rgba[(pixel * 4)     ] = data >> 24 & 0xff; //r
2819                rgba[(pixel * 4) + 1 ] = data >> 16 & 0xff; //g
2820                rgba[(pixel * 4) + 2 ] = data >> 8 & 0xff;  //b
2821                rgba[(pixel * 4) + 3 ] = data & 0xff;       //a
2822            }
2823
2824        } else {
2825            Log.Warn("The given cursor type is not supported: "
2826                      + cursorType + " given.");
2827            return false;
2828        }
2829
2830        this._updateCursor(rgba, hotx, hoty, w, h);
2831
2832        return true;
2833    }
2834
2835    _handleCursor() {
2836        const hotx = this._FBU.x;  // hotspot-x
2837        const hoty = this._FBU.y;  // hotspot-y
2838        const w = this._FBU.width;
2839        const h = this._FBU.height;
2840
2841        const pixelslength = w * h * 4;
2842        const masklength = Math.ceil(w / 8) * h;
2843
2844        let bytes = pixelslength + masklength;
2845        if (this._sock.rQwait("cursor encoding", bytes)) {
2846            return false;
2847        }
2848
2849        // Decode from BGRX pixels + bit mask to RGBA
2850        const pixels = this._sock.rQshiftBytes(pixelslength);
2851        const mask = this._sock.rQshiftBytes(masklength);
2852        let rgba = new Uint8Array(w * h * 4);
2853
2854        let pixIdx = 0;
2855        for (let y = 0; y < h; y++) {
2856            for (let x = 0; x < w; x++) {
2857                let maskIdx = y * Math.ceil(w / 8) + Math.floor(x / 8);
2858                let alpha = (mask[maskIdx] << (x % 8)) & 0x80 ? 255 : 0;
2859                rgba[pixIdx    ] = pixels[pixIdx + 2];
2860                rgba[pixIdx + 1] = pixels[pixIdx + 1];
2861                rgba[pixIdx + 2] = pixels[pixIdx];
2862                rgba[pixIdx + 3] = alpha;
2863                pixIdx += 4;
2864            }
2865        }
2866
2867        this._updateCursor(rgba, hotx, hoty, w, h);
2868
2869        return true;
2870    }
2871
2872    _handleDesktopName() {
2873        if (this._sock.rQwait("DesktopName", 4)) {
2874            return false;
2875        }
2876
2877        let length = this._sock.rQshift32();
2878
2879        if (this._sock.rQwait("DesktopName", length, 4)) {
2880            return false;
2881        }
2882
2883        let name = this._sock.rQshiftStr(length);
2884        name = decodeUTF8(name, true);
2885
2886        this._setDesktopName(name);
2887
2888        return true;
2889    }
2890
2891    _handleLedEvent() {
2892        if (this._sock.rQwait("LED status", 1)) {
2893            return false;
2894        }
2895
2896        let data = this._sock.rQshift8();
2897        // ScrollLock state can be retrieved with data & 1. This is currently not needed.
2898        let numLock = data & 2 ? true : false;
2899        let capsLock = data & 4 ? true : false;
2900        this._remoteCapsLock = capsLock;
2901        this._remoteNumLock = numLock;
2902
2903        return true;
2904    }
2905
2906    _handleExtendedDesktopSize() {
2907        if (this._sock.rQwait("ExtendedDesktopSize", 4)) {
2908            return false;
2909        }
2910
2911        const numberOfScreens = this._sock.rQpeek8();
2912
2913        let bytes = 4 + (numberOfScreens * 16);
2914        if (this._sock.rQwait("ExtendedDesktopSize", bytes)) {
2915            return false;
2916        }
2917
2918        const firstUpdate = !this._supportsSetDesktopSize;
2919        this._supportsSetDesktopSize = true;
2920
2921        this._sock.rQskipBytes(1);  // number-of-screens
2922        this._sock.rQskipBytes(3);  // padding
2923
2924        for (let i = 0; i < numberOfScreens; i += 1) {
2925            // Save the id and flags of the first screen
2926            if (i === 0) {
2927                this._screenID = this._sock.rQshift32();    // id
2928                this._sock.rQskipBytes(2);                  // x-position
2929                this._sock.rQskipBytes(2);                  // y-position
2930                this._sock.rQskipBytes(2);                  // width
2931                this._sock.rQskipBytes(2);                  // height
2932                this._screenFlags = this._sock.rQshift32(); // flags
2933            } else {
2934                this._sock.rQskipBytes(16);
2935            }
2936        }
2937
2938        /*
2939         * The x-position indicates the reason for the change:
2940         *
2941         *  0 - server resized on its own
2942         *  1 - this client requested the resize
2943         *  2 - another client requested the resize
2944         */
2945
2946        if (this._FBU.x === 1) {
2947            this._pendingRemoteResize = false;
2948        }
2949
2950        // We need to handle errors when we requested the resize.
2951        if (this._FBU.x === 1 && this._FBU.y !== 0) {
2952            let msg = "";
2953            // The y-position indicates the status code from the server
2954            switch (this._FBU.y) {
2955                case 1:
2956                    msg = "Resize is administratively prohibited";
2957                    break;
2958                case 2:
2959                    msg = "Out of resources";
2960                    break;
2961                case 3:
2962                    msg = "Invalid screen layout";
2963                    break;
2964                default:
2965                    msg = "Unknown reason";
2966                    break;
2967            }
2968            Log.Warn("Server did not accept the resize request: "
2969                     + msg);
2970        } else {
2971            this._resize(this._FBU.width, this._FBU.height);
2972        }
2973
2974        // Normally we only apply the current resize mode after a
2975        // window resize event. However there is no such trigger on the
2976        // initial connect. And we don't know if the server supports
2977        // resizing until we've gotten here.
2978        if (firstUpdate) {
2979            this._requestRemoteResize();
2980        }
2981
2982        if (this._FBU.x === 1 && this._FBU.y === 0) {
2983            // We might have resized again whilst waiting for the
2984            // previous request, so check if we are in sync
2985            this._requestRemoteResize();
2986        }
2987
2988        return true;
2989    }
2990
2991    _handleDataRect() {
2992        let decoder = this._decoders[this._FBU.encoding];
2993        if (!decoder) {
2994            this._fail("Unsupported encoding (encoding: " +
2995                       this._FBU.encoding + ")");
2996            return false;
2997        }
2998
2999        try {
3000            return decoder.decodeRect(this._FBU.x, this._FBU.y,
3001                                      this._FBU.width, this._FBU.height,
3002                                      this._sock, this._display,
3003                                      this._fbDepth);
3004        } catch (err) {
3005            this._fail("Error decoding rect: " + err);
3006            return false;
3007        }
3008    }
3009
3010    _updateContinuousUpdates() {
3011        if (!this._enabledContinuousUpdates) { return; }
3012
3013        RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
3014                                             this._fbWidth, this._fbHeight);
3015    }
3016
3017    // Handle resize-messages from the server
3018    _resize(width, height) {
3019        this._fbWidth = width;
3020        this._fbHeight = height;
3021
3022        this._display.resize(this._fbWidth, this._fbHeight);
3023
3024        // Adjust the visible viewport based on the new dimensions
3025        this._updateClip();
3026        this._updateScale();
3027
3028        this._updateContinuousUpdates();
3029
3030        // Keep this size until browser client size changes
3031        this._saveExpectedClientSize();
3032    }
3033
3034    _xvpOp(ver, op) {
3035        if (this._rfbXvpVer < ver) { return; }
3036        Log.Info("Sending XVP operation " + op + " (version " + ver + ")");
3037        RFB.messages.xvpOp(this._sock, ver, op);
3038    }
3039
3040    _updateCursor(rgba, hotx, hoty, w, h) {
3041        this._cursorImage = {
3042            rgbaPixels: rgba,
3043            hotx: hotx, hoty: hoty, w: w, h: h,
3044        };
3045        this._refreshCursor();
3046    }
3047
3048    _shouldShowDotCursor() {
3049        // Called when this._cursorImage is updated
3050        if (!this._showDotCursor) {
3051            // User does not want to see the dot, so...
3052            return false;
3053        }
3054
3055        // The dot should not be shown if the cursor is already visible,
3056        // i.e. contains at least one not-fully-transparent pixel.
3057        // So iterate through all alpha bytes in rgba and stop at the
3058        // first non-zero.
3059        for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) {
3060            if (this._cursorImage.rgbaPixels[i]) {
3061                return false;
3062            }
3063        }
3064
3065        // At this point, we know that the cursor is fully transparent, and
3066        // the user wants to see the dot instead of this.
3067        return true;
3068    }
3069
3070    _refreshCursor() {
3071        if (this._rfbConnectionState !== "connecting" &&
3072            this._rfbConnectionState !== "connected") {
3073            return;
3074        }
3075        const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage;
3076        this._cursor.change(image.rgbaPixels,
3077                            image.hotx, image.hoty,
3078                            image.w, image.h
3079        );
3080    }
3081
3082    static genDES(password, challenge) {
3083        const passwordChars = password.split('').map(c => c.charCodeAt(0));
3084        const key = legacyCrypto.importKey(
3085            "raw", passwordChars, { name: "DES-ECB" }, false, ["encrypt"]);
3086        return legacyCrypto.encrypt({ name: "DES-ECB" }, key, challenge);
3087    }
3088}
3089
3090// Class Methods
3091RFB.messages = {
3092    keyEvent(sock, keysym, down) {
3093        sock.sQpush8(4); // msg-type
3094        sock.sQpush8(down);
3095
3096        sock.sQpush16(0);
3097
3098        sock.sQpush32(keysym);
3099
3100        sock.flush();
3101    },
3102
3103    QEMUExtendedKeyEvent(sock, keysym, down, keycode) {
3104        function getRFBkeycode(xtScanCode) {
3105            const upperByte = (keycode >> 8);
3106            const lowerByte = (keycode & 0x00ff);
3107            if (upperByte === 0xe0 && lowerByte < 0x7f) {
3108                return lowerByte | 0x80;
3109            }
3110            return xtScanCode;
3111        }
3112
3113        sock.sQpush8(255); // msg-type
3114        sock.sQpush8(0); // sub msg-type
3115
3116        sock.sQpush16(down);
3117
3118        sock.sQpush32(keysym);
3119
3120        const RFBkeycode = getRFBkeycode(keycode);
3121
3122        sock.sQpush32(RFBkeycode);
3123
3124        sock.flush();
3125    },
3126
3127    pointerEvent(sock, x, y, mask) {
3128        sock.sQpush8(5); // msg-type
3129
3130        // Marker bit must be set to 0, otherwise the server might
3131        // confuse the marker bit with the highest bit in a normal
3132        // PointerEvent message.
3133        mask = mask & 0x7f;
3134        sock.sQpush8(mask);
3135
3136        sock.sQpush16(x);
3137        sock.sQpush16(y);
3138
3139        sock.flush();
3140    },
3141
3142    extendedPointerEvent(sock, x, y, mask) {
3143        sock.sQpush8(5); // msg-type
3144
3145        let higherBits = (mask >> 7) & 0xff;
3146
3147        // Bits 2-7 are reserved
3148        if (higherBits & 0xfc) {
3149            throw new Error("Invalid mouse button mask: " + mask);
3150        }
3151
3152        let lowerBits = mask & 0x7f;
3153        lowerBits |= 0x80; // Set marker bit to 1
3154
3155        sock.sQpush8(lowerBits);
3156        sock.sQpush16(x);
3157        sock.sQpush16(y);
3158        sock.sQpush8(higherBits);
3159
3160        sock.flush();
3161    },
3162
3163    // Used to build Notify and Request data.
3164    _buildExtendedClipboardFlags(actions, formats) {
3165        let data = new Uint8Array(4);
3166        let formatFlag = 0x00000000;
3167        let actionFlag = 0x00000000;
3168
3169        for (let i = 0; i < actions.length; i++) {
3170            actionFlag |= actions[i];
3171        }
3172
3173        for (let i = 0; i < formats.length; i++) {
3174            formatFlag |= formats[i];
3175        }
3176
3177        data[0] = actionFlag >> 24; // Actions
3178        data[1] = 0x00;             // Reserved
3179        data[2] = 0x00;             // Reserved
3180        data[3] = formatFlag;       // Formats
3181
3182        return data;
3183    },
3184
3185    extendedClipboardProvide(sock, formats, inData) {
3186        // Deflate incomming data and their sizes
3187        let deflator = new Deflator();
3188        let dataToDeflate = [];
3189
3190        for (let i = 0; i < formats.length; i++) {
3191            // We only support the format Text at this time
3192            if (formats[i] != extendedClipboardFormatText) {
3193                throw new Error("Unsupported extended clipboard format for Provide message.");
3194            }
3195
3196            // Change lone \r or \n into \r\n as defined in rfbproto
3197            inData[i] = inData[i].replace(/\r\n|\r|\n/gm, "\r\n");
3198
3199            // Check if it already has \0
3200            let text = encodeUTF8(inData[i] + "\0");
3201
3202            dataToDeflate.push( (text.length >> 24) & 0xFF,
3203                                (text.length >> 16) & 0xFF,
3204                                (text.length >>  8) & 0xFF,
3205                                (text.length & 0xFF));
3206
3207            for (let j = 0; j < text.length; j++) {
3208                dataToDeflate.push(text.charCodeAt(j));
3209            }
3210        }
3211
3212        let deflatedData = deflator.deflate(new Uint8Array(dataToDeflate));
3213
3214        // Build data  to send
3215        let data = new Uint8Array(4 + deflatedData.length);
3216        data.set(RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionProvide],
3217                                                           formats));
3218        data.set(deflatedData, 4);
3219
3220        RFB.messages.clientCutText(sock, data, true);
3221    },
3222
3223    extendedClipboardNotify(sock, formats) {
3224        let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionNotify],
3225                                                              formats);
3226        RFB.messages.clientCutText(sock, flags, true);
3227    },
3228
3229    extendedClipboardRequest(sock, formats) {
3230        let flags = RFB.messages._buildExtendedClipboardFlags([extendedClipboardActionRequest],
3231                                                              formats);
3232        RFB.messages.clientCutText(sock, flags, true);
3233    },
3234
3235    extendedClipboardCaps(sock, actions, formats) {
3236        let formatKeys = Object.keys(formats);
3237        let data  = new Uint8Array(4 + (4 * formatKeys.length));
3238
3239        formatKeys.map(x => parseInt(x));
3240        formatKeys.sort((a, b) =>  a - b);
3241
3242        data.set(RFB.messages._buildExtendedClipboardFlags(actions, []));
3243
3244        let loopOffset = 4;
3245        for (let i = 0; i < formatKeys.length; i++) {
3246            data[loopOffset]     = formats[formatKeys[i]] >> 24;
3247            data[loopOffset + 1] = formats[formatKeys[i]] >> 16;
3248            data[loopOffset + 2] = formats[formatKeys[i]] >> 8;
3249            data[loopOffset + 3] = formats[formatKeys[i]] >> 0;
3250
3251            loopOffset += 4;
3252            data[3] |= (1 << formatKeys[i]); // Update our format flags
3253        }
3254
3255        RFB.messages.clientCutText(sock, data, true);
3256    },
3257
3258    clientCutText(sock, data, extended = false) {
3259        sock.sQpush8(6); // msg-type
3260
3261        sock.sQpush8(0); // padding
3262        sock.sQpush8(0); // padding
3263        sock.sQpush8(0); // padding
3264
3265        let length;
3266        if (extended) {
3267            length = toUnsigned32bit(-data.length);
3268        } else {
3269            length = data.length;
3270        }
3271
3272        sock.sQpush32(length);
3273        sock.sQpushBytes(data);
3274        sock.flush();
3275    },
3276
3277    setDesktopSize(sock, width, height, id, flags) {
3278        sock.sQpush8(251); // msg-type
3279
3280        sock.sQpush8(0); // padding
3281
3282        sock.sQpush16(width);
3283        sock.sQpush16(height);
3284
3285        sock.sQpush8(1); // number-of-screens
3286
3287        sock.sQpush8(0); // padding
3288
3289        // screen array
3290        sock.sQpush32(id);
3291        sock.sQpush16(0); // x-position
3292        sock.sQpush16(0); // y-position
3293        sock.sQpush16(width);
3294        sock.sQpush16(height);
3295        sock.sQpush32(flags);
3296
3297        sock.flush();
3298    },
3299
3300    clientFence(sock, flags, payload) {
3301        sock.sQpush8(248); // msg-type
3302
3303        sock.sQpush8(0); // padding
3304        sock.sQpush8(0); // padding
3305        sock.sQpush8(0); // padding
3306
3307        sock.sQpush32(flags);
3308
3309        sock.sQpush8(payload.length);
3310        sock.sQpushString(payload);
3311
3312        sock.flush();
3313    },
3314
3315    enableContinuousUpdates(sock, enable, x, y, width, height) {
3316        sock.sQpush8(150); // msg-type
3317
3318        sock.sQpush8(enable);
3319
3320        sock.sQpush16(x);
3321        sock.sQpush16(y);
3322        sock.sQpush16(width);
3323        sock.sQpush16(height);
3324
3325        sock.flush();
3326    },
3327
3328    pixelFormat(sock, depth, trueColor) {
3329        let bpp;
3330
3331        if (depth > 16) {
3332            bpp = 32;
3333        } else if (depth > 8) {
3334            bpp = 16;
3335        } else {
3336            bpp = 8;
3337        }
3338
3339        const bits = Math.floor(depth/3);
3340
3341        sock.sQpush8(0); // msg-type
3342
3343        sock.sQpush8(0); // padding
3344        sock.sQpush8(0); // padding
3345        sock.sQpush8(0); // padding
3346
3347        sock.sQpush8(bpp);
3348        sock.sQpush8(depth);
3349        sock.sQpush8(0); // little-endian
3350        sock.sQpush8(trueColor ? 1 : 0);
3351
3352        sock.sQpush16((1 << bits) - 1); // red-max
3353        sock.sQpush16((1 << bits) - 1); // green-max
3354        sock.sQpush16((1 << bits) - 1); // blue-max
3355
3356        sock.sQpush8(bits * 0); // red-shift
3357        sock.sQpush8(bits * 1); // green-shift
3358        sock.sQpush8(bits * 2); // blue-shift
3359
3360        sock.sQpush8(0); // padding
3361        sock.sQpush8(0); // padding
3362        sock.sQpush8(0); // padding
3363
3364        sock.flush();
3365    },
3366
3367    clientEncodings(sock, encodings) {
3368        sock.sQpush8(2); // msg-type
3369
3370        sock.sQpush8(0); // padding
3371
3372        sock.sQpush16(encodings.length);
3373        for (let i = 0; i < encodings.length; i++) {
3374            sock.sQpush32(encodings[i]);
3375        }
3376
3377        sock.flush();
3378    },
3379
3380    fbUpdateRequest(sock, incremental, x, y, w, h) {
3381        if (typeof(x) === "undefined") { x = 0; }
3382        if (typeof(y) === "undefined") { y = 0; }
3383
3384        sock.sQpush8(3); // msg-type
3385
3386        sock.sQpush8(incremental ? 1 : 0);
3387
3388        sock.sQpush16(x);
3389        sock.sQpush16(y);
3390        sock.sQpush16(w);
3391        sock.sQpush16(h);
3392
3393        sock.flush();
3394    },
3395
3396    xvpOp(sock, ver, op) {
3397        sock.sQpush8(250); // msg-type
3398
3399        sock.sQpush8(0); // padding
3400
3401        sock.sQpush8(ver);
3402        sock.sQpush8(op);
3403
3404        sock.flush();
3405    }
3406};
3407
3408RFB.cursors = {
3409    none: {
3410        rgbaPixels: new Uint8Array(),
3411        w: 0, h: 0,
3412        hotx: 0, hoty: 0,
3413    },
3414
3415    dot: {
3416        /* eslint-disable indent */
3417        rgbaPixels: new Uint8Array([
3418            255, 255, 255, 255,   0,   0,   0, 255, 255, 255, 255, 255,
3419              0,   0,   0, 255,   0,   0,   0,   0,   0,   0,  0,  255,
3420            255, 255, 255, 255,   0,   0,   0, 255, 255, 255, 255, 255,
3421        ]),
3422        /* eslint-enable indent */
3423        w: 3, h: 3,
3424        hotx: 1, hoty: 1,
3425    }
3426};