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