| /* |
| * noVNC: HTML5 VNC client |
| * Copyright (C) 2018 The noVNC Authors |
| * Licensed under MPL 2.0 (see LICENSE.txt) |
| * |
| * See README.md for usage and integration instructions. |
| * |
| */ |
| |
| import * as Log from './util/logging.js'; |
| import { decodeUTF8 } from './util/strings.js'; |
| import { dragThreshold } from './util/browser.js'; |
| import EventTargetMixin from './util/eventtarget.js'; |
| import Display from "./display.js"; |
| import Keyboard from "./input/keyboard.js"; |
| import Mouse from "./input/mouse.js"; |
| import Cursor from "./util/cursor.js"; |
| import Websock from "./websock.js"; |
| import DES from "./des.js"; |
| import KeyTable from "./input/keysym.js"; |
| import XtScancode from "./input/xtscancodes.js"; |
| import { encodings } from "./encodings.js"; |
| import "./util/polyfill.js"; |
| |
| import RawDecoder from "./decoders/raw.js"; |
| import CopyRectDecoder from "./decoders/copyrect.js"; |
| import RREDecoder from "./decoders/rre.js"; |
| import HextileDecoder from "./decoders/hextile.js"; |
| import TightDecoder from "./decoders/tight.js"; |
| import TightPNGDecoder from "./decoders/tightpng.js"; |
| |
| // How many seconds to wait for a disconnect to finish |
| const DISCONNECT_TIMEOUT = 3; |
| const DEFAULT_BACKGROUND = 'rgb(40, 40, 40)'; |
| |
| export default class RFB extends EventTargetMixin { |
| constructor(target, url, options) { |
| if (!target) { |
| throw new Error("Must specify target"); |
| } |
| if (!url) { |
| throw new Error("Must specify URL"); |
| } |
| |
| super(); |
| |
| this._target = target; |
| this._url = url; |
| |
| // Connection details |
| options = options || {}; |
| this._rfb_credentials = options.credentials || {}; |
| this._shared = false; |
| this._repeaterID = options.repeaterID || ''; |
| this._showDotCursor = options.showDotCursor || false; |
| |
| // Internal state |
| this._rfb_connection_state = ''; |
| this._rfb_init_state = ''; |
| this._rfb_auth_scheme = -1; |
| this._rfb_clean_disconnect = true; |
| |
| // Server capabilities |
| this._rfb_version = 0; |
| this._rfb_max_version = 3.8; |
| this._rfb_tightvnc = false; |
| this._rfb_xvp_ver = 0; |
| |
| this._fb_width = 0; |
| this._fb_height = 0; |
| |
| this._fb_name = ""; |
| |
| this._capabilities = { power: false }; |
| |
| this._supportsFence = false; |
| |
| this._supportsContinuousUpdates = false; |
| this._enabledContinuousUpdates = false; |
| |
| this._supportsSetDesktopSize = false; |
| this._screen_id = 0; |
| this._screen_flags = 0; |
| |
| this._qemuExtKeyEventSupported = false; |
| |
| // Internal objects |
| this._sock = null; // Websock object |
| this._display = null; // Display object |
| this._flushing = false; // Display flushing state |
| this._keyboard = null; // Keyboard input handler object |
| this._mouse = null; // Mouse input handler object |
| |
| // Timers |
| this._disconnTimer = null; // disconnection timer |
| this._resizeTimeout = null; // resize rate limiting |
| |
| // Decoder states |
| this._decoders = {}; |
| |
| this._FBU = { |
| rects: 0, |
| x: 0, |
| y: 0, |
| width: 0, |
| height: 0, |
| encoding: null, |
| }; |
| |
| // Mouse state |
| this._mouse_buttonMask = 0; |
| this._mouse_arr = []; |
| this._viewportDragging = false; |
| this._viewportDragPos = {}; |
| this._viewportHasMoved = false; |
| |
| // Bound event handlers |
| this._eventHandlers = { |
| focusCanvas: this._focusCanvas.bind(this), |
| windowResize: this._windowResize.bind(this), |
| }; |
| |
| // main setup |
| Log.Debug(">> RFB.constructor"); |
| |
| // Create DOM elements |
| this._screen = document.createElement('div'); |
| this._screen.style.display = 'flex'; |
| this._screen.style.width = '100%'; |
| this._screen.style.height = '100%'; |
| this._screen.style.overflow = 'auto'; |
| this._screen.style.background = DEFAULT_BACKGROUND; |
| this._canvas = document.createElement('canvas'); |
| this._canvas.style.margin = 'auto'; |
| // Some browsers add an outline on focus |
| this._canvas.style.outline = 'none'; |
| // IE miscalculates width without this :( |
| this._canvas.style.flexShrink = '0'; |
| this._canvas.width = 0; |
| this._canvas.height = 0; |
| this._canvas.tabIndex = -1; |
| this._screen.appendChild(this._canvas); |
| |
| // Cursor |
| this._cursor = new Cursor(); |
| |
| // XXX: TightVNC 2.8.11 sends no cursor at all until Windows changes |
| // it. Result: no cursor at all until a window border or an edit field |
| // is hit blindly. But there are also VNC servers that draw the cursor |
| // in the framebuffer and don't send the empty local cursor. There is |
| // no way to satisfy both sides. |
| // |
| // The spec is unclear on this "initial cursor" issue. Many other |
| // viewers (TigerVNC, RealVNC, Remmina) display an arrow as the |
| // initial cursor instead. |
| this._cursorImage = RFB.cursors.none; |
| |
| // populate decoder array with objects |
| this._decoders[encodings.encodingRaw] = new RawDecoder(); |
| this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder(); |
| this._decoders[encodings.encodingRRE] = new RREDecoder(); |
| this._decoders[encodings.encodingHextile] = new HextileDecoder(); |
| this._decoders[encodings.encodingTight] = new TightDecoder(); |
| this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder(); |
| |
| // NB: nothing that needs explicit teardown should be done |
| // before this point, since this can throw an exception |
| try { |
| this._display = new Display(this._canvas); |
| } catch (exc) { |
| Log.Error("Display exception: " + exc); |
| throw exc; |
| } |
| this._display.onflush = this._onFlush.bind(this); |
| this._display.clear(); |
| |
| this._keyboard = new Keyboard(this._canvas); |
| this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); |
| |
| this._mouse = new Mouse(this._canvas); |
| this._mouse.onmousebutton = this._handleMouseButton.bind(this); |
| this._mouse.onmousemove = this._handleMouseMove.bind(this); |
| |
| this._sock = new Websock(); |
| this._sock.on('message', () => { |
| this._handle_message(); |
| }); |
| this._sock.on('open', () => { |
| if ((this._rfb_connection_state === 'connecting') && |
| (this._rfb_init_state === '')) { |
| this._rfb_init_state = 'ProtocolVersion'; |
| Log.Debug("Starting VNC handshake"); |
| } else { |
| this._fail("Unexpected server connection while " + |
| this._rfb_connection_state); |
| } |
| }); |
| this._sock.on('close', (e) => { |
| Log.Debug("WebSocket on-close event"); |
| let msg = ""; |
| if (e.code) { |
| msg = "(code: " + e.code; |
| if (e.reason) { |
| msg += ", reason: " + e.reason; |
| } |
| msg += ")"; |
| } |
| switch (this._rfb_connection_state) { |
| case 'connecting': |
| this._fail("Connection closed " + msg); |
| break; |
| case 'connected': |
| // Handle disconnects that were initiated server-side |
| this._updateConnectionState('disconnecting'); |
| this._updateConnectionState('disconnected'); |
| break; |
| case 'disconnecting': |
| // Normal disconnection path |
| this._updateConnectionState('disconnected'); |
| break; |
| case 'disconnected': |
| this._fail("Unexpected server disconnect " + |
| "when already disconnected " + msg); |
| break; |
| default: |
| this._fail("Unexpected server disconnect before connecting " + |
| msg); |
| break; |
| } |
| this._sock.off('close'); |
| }); |
| this._sock.on('error', e => Log.Warn("WebSocket on-error event")); |
| |
| // Slight delay of the actual connection so that the caller has |
| // time to set up callbacks |
| setTimeout(this._updateConnectionState.bind(this, 'connecting')); |
| |
| Log.Debug("<< RFB.constructor"); |
| |
| // ===== PROPERTIES ===== |
| |
| this.dragViewport = false; |
| this.focusOnClick = true; |
| |
| this._viewOnly = false; |
| this._clipViewport = false; |
| this._scaleViewport = false; |
| this._resizeSession = false; |
| } |
| |
| // ===== PROPERTIES ===== |
| |
| get viewOnly() { return this._viewOnly; } |
| set viewOnly(viewOnly) { |
| this._viewOnly = viewOnly; |
| |
| if (this._rfb_connection_state === "connecting" || |
| this._rfb_connection_state === "connected") { |
| if (viewOnly) { |
| this._keyboard.ungrab(); |
| this._mouse.ungrab(); |
| } else { |
| this._keyboard.grab(); |
| this._mouse.grab(); |
| } |
| } |
| } |
| |
| get capabilities() { return this._capabilities; } |
| |
| get touchButton() { return this._mouse.touchButton; } |
| set touchButton(button) { this._mouse.touchButton = button; } |
| |
| get clipViewport() { return this._clipViewport; } |
| set clipViewport(viewport) { |
| this._clipViewport = viewport; |
| this._updateClip(); |
| } |
| |
| get scaleViewport() { return this._scaleViewport; } |
| set scaleViewport(scale) { |
| this._scaleViewport = scale; |
| // Scaling trumps clipping, so we may need to adjust |
| // clipping when enabling or disabling scaling |
| if (scale && this._clipViewport) { |
| this._updateClip(); |
| } |
| this._updateScale(); |
| if (!scale && this._clipViewport) { |
| this._updateClip(); |
| } |
| } |
| |
| get resizeSession() { return this._resizeSession; } |
| set resizeSession(resize) { |
| this._resizeSession = resize; |
| if (resize) { |
| this._requestRemoteResize(); |
| } |
| } |
| |
| get showDotCursor() { return this._showDotCursor; } |
| set showDotCursor(show) { |
| this._showDotCursor = show; |
| this._refreshCursor(); |
| } |
| |
| get background() { return this._screen.style.background; } |
| set background(cssValue) { this._screen.style.background = cssValue; } |
| |
| // ===== PUBLIC METHODS ===== |
| |
| disconnect() { |
| this._updateConnectionState('disconnecting'); |
| this._sock.off('error'); |
| this._sock.off('message'); |
| this._sock.off('open'); |
| } |
| |
| sendCredentials(creds) { |
| this._rfb_credentials = creds; |
| setTimeout(this._init_msg.bind(this), 0); |
| } |
| |
| sendCtrlAltDel() { |
| if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } |
| Log.Info("Sending Ctrl-Alt-Del"); |
| |
| this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); |
| this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true); |
| this.sendKey(KeyTable.XK_Delete, "Delete", true); |
| this.sendKey(KeyTable.XK_Delete, "Delete", false); |
| this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false); |
| this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false); |
| } |
| |
| sendCtrlEsc() { |
| if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } |
| Log.Info("Sending Ctrl-Esc"); |
| |
| this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); |
| this.sendKey(KeyTable.XK_Escape, "Escape", true); |
| this.sendKey(KeyTable.XK_Escape, "Escape", false); |
| this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false); |
| } |
| |
| machineShutdown() { |
| this._xvpOp(1, 2); |
| } |
| |
| machineReboot() { |
| this._xvpOp(1, 3); |
| } |
| |
| machineReset() { |
| this._xvpOp(1, 4); |
| } |
| |
| // Send a key press. If 'down' is not specified then send a down key |
| // followed by an up key. |
| sendKey(keysym, code, down) { |
| if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } |
| |
| if (down === undefined) { |
| this.sendKey(keysym, code, true); |
| this.sendKey(keysym, code, false); |
| return; |
| } |
| |
| const scancode = XtScancode[code]; |
| |
| if (this._qemuExtKeyEventSupported && scancode) { |
| // 0 is NoSymbol |
| keysym = keysym || 0; |
| |
| Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode); |
| |
| RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode); |
| } else { |
| if (!keysym) { |
| return; |
| } |
| Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym); |
| RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); |
| } |
| } |
| |
| focus() { |
| this._canvas.focus(); |
| } |
| |
| blur() { |
| this._canvas.blur(); |
| } |
| |
| clipboardPasteFrom(text) { |
| if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } |
| RFB.messages.clientCutText(this._sock, text); |
| } |
| |
| // ===== PRIVATE METHODS ===== |
| |
| _connect() { |
| Log.Debug(">> RFB.connect"); |
| |
| Log.Info("connecting to " + this._url); |
| |
| try { |
| // WebSocket.onopen transitions to the RFB init states |
| this._sock.open(this._url, ['binary']); |
| } catch (e) { |
| if (e.name === 'SyntaxError') { |
| this._fail("Invalid host or port (" + e + ")"); |
| } else { |
| this._fail("Error when opening socket (" + e + ")"); |
| } |
| } |
| |
| // Make our elements part of the page |
| this._target.appendChild(this._screen); |
| |
| this._cursor.attach(this._canvas); |
| this._refreshCursor(); |
| |
| // Monitor size changes of the screen |
| // FIXME: Use ResizeObserver, or hidden overflow |
| window.addEventListener('resize', this._eventHandlers.windowResize); |
| |
| // Always grab focus on some kind of click event |
| this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas); |
| this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas); |
| |
| Log.Debug("<< RFB.connect"); |
| } |
| |
| _disconnect() { |
| Log.Debug(">> RFB.disconnect"); |
| this._cursor.detach(); |
| this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas); |
| this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas); |
| window.removeEventListener('resize', this._eventHandlers.windowResize); |
| this._keyboard.ungrab(); |
| this._mouse.ungrab(); |
| this._sock.close(); |
| try { |
| this._target.removeChild(this._screen); |
| } catch (e) { |
| if (e.name === 'NotFoundError') { |
| // Some cases where the initial connection fails |
| // can disconnect before the _screen is created |
| } else { |
| throw e; |
| } |
| } |
| clearTimeout(this._resizeTimeout); |
| Log.Debug("<< RFB.disconnect"); |
| } |
| |
| _focusCanvas(event) { |
| // Respect earlier handlers' request to not do side-effects |
| if (event.defaultPrevented) { |
| return; |
| } |
| |
| if (!this.focusOnClick) { |
| return; |
| } |
| |
| this.focus(); |
| } |
| |
| _windowResize(event) { |
| // If the window resized then our screen element might have |
| // as well. Update the viewport dimensions. |
| window.requestAnimationFrame(() => { |
| this._updateClip(); |
| this._updateScale(); |
| }); |
| |
| if (this._resizeSession) { |
| // Request changing the resolution of the remote display to |
| // the size of the local browser viewport. |
| |
| // In order to not send multiple requests before the browser-resize |
| // is finished we wait 0.5 seconds before sending the request. |
| clearTimeout(this._resizeTimeout); |
| this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500); |
| } |
| } |
| |
| // Update state of clipping in Display object, and make sure the |
| // configured viewport matches the current screen size |
| _updateClip() { |
| const cur_clip = this._display.clipViewport; |
| let new_clip = this._clipViewport; |
| |
| if (this._scaleViewport) { |
| // Disable viewport clipping if we are scaling |
| new_clip = false; |
| } |
| |
| if (cur_clip !== new_clip) { |
| this._display.clipViewport = new_clip; |
| } |
| |
| if (new_clip) { |
| // When clipping is enabled, the screen is limited to |
| // the size of the container. |
| const size = this._screenSize(); |
| this._display.viewportChangeSize(size.w, size.h); |
| this._fixScrollbars(); |
| } |
| } |
| |
| _updateScale() { |
| if (!this._scaleViewport) { |
| this._display.scale = 1.0; |
| } else { |
| const size = this._screenSize(); |
| this._display.autoscale(size.w, size.h); |
| } |
| this._fixScrollbars(); |
| } |
| |
| // Requests a change of remote desktop size. This message is an extension |
| // and may only be sent if we have received an ExtendedDesktopSize message |
| _requestRemoteResize() { |
| clearTimeout(this._resizeTimeout); |
| this._resizeTimeout = null; |
| |
| if (!this._resizeSession || this._viewOnly || |
| !this._supportsSetDesktopSize) { |
| return; |
| } |
| |
| const size = this._screenSize(); |
| RFB.messages.setDesktopSize(this._sock, |
| Math.floor(size.w), Math.floor(size.h), |
| this._screen_id, this._screen_flags); |
| |
| Log.Debug('Requested new desktop size: ' + |
| size.w + 'x' + size.h); |
| } |
| |
| // Gets the the size of the available screen |
| _screenSize() { |
| let r = this._screen.getBoundingClientRect(); |
| return { w: r.width, h: r.height }; |
| } |
| |
| _fixScrollbars() { |
| // This is a hack because Chrome screws up the calculation |
| // for when scrollbars are needed. So to fix it we temporarily |
| // toggle them off and on. |
| const orig = this._screen.style.overflow; |
| this._screen.style.overflow = 'hidden'; |
| // Force Chrome to recalculate the layout by asking for |
| // an element's dimensions |
| this._screen.getBoundingClientRect(); |
| this._screen.style.overflow = orig; |
| } |
| |
| /* |
| * Connection states: |
| * connecting |
| * connected |
| * disconnecting |
| * disconnected - permanent state |
| */ |
| _updateConnectionState(state) { |
| const oldstate = this._rfb_connection_state; |
| |
| if (state === oldstate) { |
| Log.Debug("Already in state '" + state + "', ignoring"); |
| return; |
| } |
| |
| // The 'disconnected' state is permanent for each RFB object |
| if (oldstate === 'disconnected') { |
| Log.Error("Tried changing state of a disconnected RFB object"); |
| return; |
| } |
| |
| // Ensure proper transitions before doing anything |
| switch (state) { |
| case 'connected': |
| if (oldstate !== 'connecting') { |
| Log.Error("Bad transition to connected state, " + |
| "previous connection state: " + oldstate); |
| return; |
| } |
| break; |
| |
| case 'disconnected': |
| if (oldstate !== 'disconnecting') { |
| Log.Error("Bad transition to disconnected state, " + |
| "previous connection state: " + oldstate); |
| return; |
| } |
| break; |
| |
| case 'connecting': |
| if (oldstate !== '') { |
| Log.Error("Bad transition to connecting state, " + |
| "previous connection state: " + oldstate); |
| return; |
| } |
| break; |
| |
| case 'disconnecting': |
| if (oldstate !== 'connected' && oldstate !== 'connecting') { |
| Log.Error("Bad transition to disconnecting state, " + |
| "previous connection state: " + oldstate); |
| return; |
| } |
| break; |
| |
| default: |
| Log.Error("Unknown connection state: " + state); |
| return; |
| } |
| |
| // State change actions |
| |
| this._rfb_connection_state = state; |
| |
| Log.Debug("New state '" + state + "', was '" + oldstate + "'."); |
| |
| if (this._disconnTimer && state !== 'disconnecting') { |
| Log.Debug("Clearing disconnect timer"); |
| clearTimeout(this._disconnTimer); |
| this._disconnTimer = null; |
| |
| // make sure we don't get a double event |
| this._sock.off('close'); |
| } |
| |
| switch (state) { |
| case 'connecting': |
| this._connect(); |
| break; |
| |
| case 'connected': |
| this.dispatchEvent(new CustomEvent("connect", { detail: {} })); |
| break; |
| |
| case 'disconnecting': |
| this._disconnect(); |
| |
| this._disconnTimer = setTimeout(() => { |
| Log.Error("Disconnection timed out."); |
| this._updateConnectionState('disconnected'); |
| }, DISCONNECT_TIMEOUT * 1000); |
| break; |
| |
| case 'disconnected': |
| this.dispatchEvent(new CustomEvent( |
| "disconnect", { detail: |
| { clean: this._rfb_clean_disconnect } })); |
| break; |
| } |
| } |
| |
| /* Print errors and disconnect |
| * |
| * The parameter 'details' is used for information that |
| * should be logged but not sent to the user interface. |
| */ |
| _fail(details) { |
| switch (this._rfb_connection_state) { |
| case 'disconnecting': |
| Log.Error("Failed when disconnecting: " + details); |
| break; |
| case 'connected': |
| Log.Error("Failed while connected: " + details); |
| break; |
| case 'connecting': |
| Log.Error("Failed when connecting: " + details); |
| break; |
| default: |
| Log.Error("RFB failure: " + details); |
| break; |
| } |
| this._rfb_clean_disconnect = false; //This is sent to the UI |
| |
| // Transition to disconnected without waiting for socket to close |
| this._updateConnectionState('disconnecting'); |
| this._updateConnectionState('disconnected'); |
| |
| return false; |
| } |
| |
| _setCapability(cap, val) { |
| this._capabilities[cap] = val; |
| this.dispatchEvent(new CustomEvent("capabilities", |
| { detail: { capabilities: this._capabilities } })); |
| } |
| |
| _handle_message() { |
| if (this._sock.rQlen === 0) { |
| Log.Warn("handle_message called on an empty receive queue"); |
| return; |
| } |
| |
| switch (this._rfb_connection_state) { |
| case 'disconnected': |
| Log.Error("Got data while disconnected"); |
| break; |
| case 'connected': |
| while (true) { |
| if (this._flushing) { |
| break; |
| } |
| if (!this._normal_msg()) { |
| break; |
| } |
| if (this._sock.rQlen === 0) { |
| break; |
| } |
| } |
| break; |
| default: |
| this._init_msg(); |
| break; |
| } |
| } |
| |
| _handleKeyEvent(keysym, code, down) { |
| this.sendKey(keysym, code, down); |
| } |
| |
| _handleMouseButton(x, y, down, bmask) { |
| if (down) { |
| this._mouse_buttonMask |= bmask; |
| } else { |
| this._mouse_buttonMask &= ~bmask; |
| } |
| |
| if (this.dragViewport) { |
| if (down && !this._viewportDragging) { |
| this._viewportDragging = true; |
| this._viewportDragPos = {'x': x, 'y': y}; |
| this._viewportHasMoved = false; |
| |
| // Skip sending mouse events |
| return; |
| } else { |
| this._viewportDragging = false; |
| |
| // If we actually performed a drag then we are done |
| // here and should not send any mouse events |
| if (this._viewportHasMoved) { |
| return; |
| } |
| |
| // Otherwise we treat this as a mouse click event. |
| // Send the button down event here, as the button up |
| // event is sent at the end of this function. |
| RFB.messages.pointerEvent(this._sock, |
| this._display.absX(x), |
| this._display.absY(y), |
| bmask); |
| } |
| } |
| |
| if (this._viewOnly) { return; } // View only, skip mouse events |
| |
| if (this._rfb_connection_state !== 'connected') { return; } |
| RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); |
| } |
| |
| _handleMouseMove(x, y) { |
| if (this._viewportDragging) { |
| const deltaX = this._viewportDragPos.x - x; |
| const deltaY = this._viewportDragPos.y - y; |
| |
| if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || |
| Math.abs(deltaY) > dragThreshold)) { |
| this._viewportHasMoved = true; |
| |
| this._viewportDragPos = {'x': x, 'y': y}; |
| this._display.viewportChangePos(deltaX, deltaY); |
| } |
| |
| // Skip sending mouse events |
| return; |
| } |
| |
| if (this._viewOnly) { return; } // View only, skip mouse events |
| |
| if (this._rfb_connection_state !== 'connected') { return; } |
| RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); |
| } |
| |
| // Message Handlers |
| |
| _negotiate_protocol_version() { |
| if (this._sock.rQwait("version", 12)) { |
| return false; |
| } |
| |
| const sversion = this._sock.rQshiftStr(12).substr(4, 7); |
| Log.Info("Server ProtocolVersion: " + sversion); |
| let is_repeater = 0; |
| switch (sversion) { |
| case "000.000": // UltraVNC repeater |
| is_repeater = 1; |
| break; |
| case "003.003": |
| case "003.006": // UltraVNC |
| case "003.889": // Apple Remote Desktop |
| this._rfb_version = 3.3; |
| break; |
| case "003.007": |
| this._rfb_version = 3.7; |
| break; |
| case "003.008": |
| case "004.000": // Intel AMT KVM |
| case "004.001": // RealVNC 4.6 |
| case "005.000": // RealVNC 5.3 |
| this._rfb_version = 3.8; |
| break; |
| default: |
| return this._fail("Invalid server version " + sversion); |
| } |
| |
| if (is_repeater) { |
| let repeaterID = "ID:" + this._repeaterID; |
| while (repeaterID.length < 250) { |
| repeaterID += "\0"; |
| } |
| this._sock.send_string(repeaterID); |
| return true; |
| } |
| |
| if (this._rfb_version > this._rfb_max_version) { |
| this._rfb_version = this._rfb_max_version; |
| } |
| |
| const cversion = "00" + parseInt(this._rfb_version, 10) + |
| ".00" + ((this._rfb_version * 10) % 10); |
| this._sock.send_string("RFB " + cversion + "\n"); |
| Log.Debug('Sent ProtocolVersion: ' + cversion); |
| |
| this._rfb_init_state = 'Security'; |
| } |
| |
| _negotiate_security() { |
| // Polyfill since IE and PhantomJS doesn't have |
| // TypedArray.includes() |
| function includes(item, array) { |
| for (let i = 0; i < array.length; i++) { |
| if (array[i] === item) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| if (this._rfb_version >= 3.7) { |
| // Server sends supported list, client decides |
| const num_types = this._sock.rQshift8(); |
| if (this._sock.rQwait("security type", num_types, 1)) { return false; } |
| |
| if (num_types === 0) { |
| this._rfb_init_state = "SecurityReason"; |
| this._security_context = "no security types"; |
| this._security_status = 1; |
| return this._init_msg(); |
| } |
| |
| const types = this._sock.rQshiftBytes(num_types); |
| Log.Debug("Server security types: " + types); |
| |
| // Look for each auth in preferred order |
| if (includes(1, types)) { |
| this._rfb_auth_scheme = 1; // None |
| } else if (includes(22, types)) { |
| this._rfb_auth_scheme = 22; // XVP |
| } else if (includes(16, types)) { |
| this._rfb_auth_scheme = 16; // Tight |
| } else if (includes(2, types)) { |
| this._rfb_auth_scheme = 2; // VNC Auth |
| } else { |
| return this._fail("Unsupported security types (types: " + types + ")"); |
| } |
| |
| this._sock.send([this._rfb_auth_scheme]); |
| } else { |
| // Server decides |
| if (this._sock.rQwait("security scheme", 4)) { return false; } |
| this._rfb_auth_scheme = this._sock.rQshift32(); |
| |
| if (this._rfb_auth_scheme == 0) { |
| this._rfb_init_state = "SecurityReason"; |
| this._security_context = "authentication scheme"; |
| this._security_status = 1; |
| return this._init_msg(); |
| } |
| } |
| |
| this._rfb_init_state = 'Authentication'; |
| Log.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme); |
| |
| return this._init_msg(); // jump to authentication |
| } |
| |
| _handle_security_reason() { |
| if (this._sock.rQwait("reason length", 4)) { |
| return false; |
| } |
| const strlen = this._sock.rQshift32(); |
| let reason = ""; |
| |
| if (strlen > 0) { |
| if (this._sock.rQwait("reason", strlen, 4)) { return false; } |
| reason = this._sock.rQshiftStr(strlen); |
| } |
| |
| if (reason !== "") { |
| this.dispatchEvent(new CustomEvent( |
| "securityfailure", |
| { detail: { status: this._security_status, |
| reason: reason } })); |
| |
| return this._fail("Security negotiation failed on " + |
| this._security_context + |
| " (reason: " + reason + ")"); |
| } else { |
| this.dispatchEvent(new CustomEvent( |
| "securityfailure", |
| { detail: { status: this._security_status } })); |
| |
| return this._fail("Security negotiation failed on " + |
| this._security_context); |
| } |
| } |
| |
| // authentication |
| _negotiate_xvp_auth() { |
| if (!this._rfb_credentials.username || |
| !this._rfb_credentials.password || |
| !this._rfb_credentials.target) { |
| this.dispatchEvent(new CustomEvent( |
| "credentialsrequired", |
| { detail: { types: ["username", "password", "target"] } })); |
| return false; |
| } |
| |
| const xvp_auth_str = String.fromCharCode(this._rfb_credentials.username.length) + |
| String.fromCharCode(this._rfb_credentials.target.length) + |
| this._rfb_credentials.username + |
| this._rfb_credentials.target; |
| this._sock.send_string(xvp_auth_str); |
| this._rfb_auth_scheme = 2; |
| return this._negotiate_authentication(); |
| } |
| |
| _negotiate_std_vnc_auth() { |
| if (this._sock.rQwait("auth challenge", 16)) { return false; } |
| |
| if (!this._rfb_credentials.password) { |
| this.dispatchEvent(new CustomEvent( |
| "credentialsrequired", |
| { detail: { types: ["password"] } })); |
| return false; |
| } |
| |
| // TODO(directxman12): make genDES not require an Array |
| const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); |
| const response = RFB.genDES(this._rfb_credentials.password, challenge); |
| this._sock.send(response); |
| this._rfb_init_state = "SecurityResult"; |
| return true; |
| } |
| |
| _negotiate_tight_tunnels(numTunnels) { |
| const clientSupportedTunnelTypes = { |
| 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } |
| }; |
| const serverSupportedTunnelTypes = {}; |
| // receive tunnel capabilities |
| for (let i = 0; i < numTunnels; i++) { |
| const cap_code = this._sock.rQshift32(); |
| const cap_vendor = this._sock.rQshiftStr(4); |
| const cap_signature = this._sock.rQshiftStr(8); |
| serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; |
| } |
| |
| Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes); |
| |
| // Siemens touch panels have a VNC server that supports NOTUNNEL, |
| // but forgets to advertise it. Try to detect such servers by |
| // looking for their custom tunnel type. |
| if (serverSupportedTunnelTypes[1] && |
| (serverSupportedTunnelTypes[1].vendor === "SICR") && |
| (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) { |
| Log.Debug("Detected Siemens server. Assuming NOTUNNEL support."); |
| serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' }; |
| } |
| |
| // choose the notunnel type |
| if (serverSupportedTunnelTypes[0]) { |
| if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || |
| serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { |
| return this._fail("Client's tunnel type had the incorrect " + |
| "vendor or signature"); |
| } |
| Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]); |
| this._sock.send([0, 0, 0, 0]); // use NOTUNNEL |
| return false; // wait until we receive the sub auth count to continue |
| } else { |
| return this._fail("Server wanted tunnels, but doesn't support " + |
| "the notunnel type"); |
| } |
| } |
| |
| _negotiate_tight_auth() { |
| if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation |
| if (this._sock.rQwait("num tunnels", 4)) { return false; } |
| const numTunnels = this._sock.rQshift32(); |
| if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } |
| |
| this._rfb_tightvnc = true; |
| |
| if (numTunnels > 0) { |
| this._negotiate_tight_tunnels(numTunnels); |
| return false; // wait until we receive the sub auth to continue |
| } |
| } |
| |
| // second pass, do the sub-auth negotiation |
| if (this._sock.rQwait("sub auth count", 4)) { return false; } |
| const subAuthCount = this._sock.rQshift32(); |
| if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected |
| this._rfb_init_state = 'SecurityResult'; |
| return true; |
| } |
| |
| if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } |
| |
| const clientSupportedTypes = { |
| 'STDVNOAUTH__': 1, |
| 'STDVVNCAUTH_': 2 |
| }; |
| |
| const serverSupportedTypes = []; |
| |
| for (let i = 0; i < subAuthCount; i++) { |
| this._sock.rQshift32(); // capNum |
| const capabilities = this._sock.rQshiftStr(12); |
| serverSupportedTypes.push(capabilities); |
| } |
| |
| Log.Debug("Server Tight authentication types: " + serverSupportedTypes); |
| |
| for (let authType in clientSupportedTypes) { |
| if (serverSupportedTypes.indexOf(authType) != -1) { |
| this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); |
| Log.Debug("Selected authentication type: " + authType); |
| |
| switch (authType) { |
| case 'STDVNOAUTH__': // no auth |
| this._rfb_init_state = 'SecurityResult'; |
| return true; |
| case 'STDVVNCAUTH_': // VNC auth |
| this._rfb_auth_scheme = 2; |
| return this._init_msg(); |
| default: |
| return this._fail("Unsupported tiny auth scheme " + |
| "(scheme: " + authType + ")"); |
| } |
| } |
| } |
| |
| return this._fail("No supported sub-auth types!"); |
| } |
| |
| _negotiate_authentication() { |
| switch (this._rfb_auth_scheme) { |
| case 1: // no auth |
| if (this._rfb_version >= 3.8) { |
| this._rfb_init_state = 'SecurityResult'; |
| return true; |
| } |
| this._rfb_init_state = 'ClientInitialisation'; |
| return this._init_msg(); |
| |
| case 22: // XVP auth |
| return this._negotiate_xvp_auth(); |
| |
| case 2: // VNC authentication |
| return this._negotiate_std_vnc_auth(); |
| |
| case 16: // TightVNC Security Type |
| return this._negotiate_tight_auth(); |
| |
| default: |
| return this._fail("Unsupported auth scheme (scheme: " + |
| this._rfb_auth_scheme + ")"); |
| } |
| } |
| |
| _handle_security_result() { |
| if (this._sock.rQwait('VNC auth response ', 4)) { return false; } |
| |
| const status = this._sock.rQshift32(); |
| |
| if (status === 0) { // OK |
| this._rfb_init_state = 'ClientInitialisation'; |
| Log.Debug('Authentication OK'); |
| return this._init_msg(); |
| } else { |
| if (this._rfb_version >= 3.8) { |
| this._rfb_init_state = "SecurityReason"; |
| this._security_context = "security result"; |
| this._security_status = status; |
| return this._init_msg(); |
| } else { |
| this.dispatchEvent(new CustomEvent( |
| "securityfailure", |
| { detail: { status: status } })); |
| |
| return this._fail("Security handshake failed"); |
| } |
| } |
| } |
| |
| _negotiate_server_init() { |
| if (this._sock.rQwait("server initialization", 24)) { return false; } |
| |
| /* Screen size */ |
| const width = this._sock.rQshift16(); |
| const height = this._sock.rQshift16(); |
| |
| /* PIXEL_FORMAT */ |
| const bpp = this._sock.rQshift8(); |
| const depth = this._sock.rQshift8(); |
| const big_endian = this._sock.rQshift8(); |
| const true_color = this._sock.rQshift8(); |
| |
| const red_max = this._sock.rQshift16(); |
| const green_max = this._sock.rQshift16(); |
| const blue_max = this._sock.rQshift16(); |
| const red_shift = this._sock.rQshift8(); |
| const green_shift = this._sock.rQshift8(); |
| const blue_shift = this._sock.rQshift8(); |
| this._sock.rQskipBytes(3); // padding |
| |
| // NB(directxman12): we don't want to call any callbacks or print messages until |
| // *after* we're past the point where we could backtrack |
| |
| /* Connection name/title */ |
| const name_length = this._sock.rQshift32(); |
| if (this._sock.rQwait('server init name', name_length, 24)) { return false; } |
| this._fb_name = decodeUTF8(this._sock.rQshiftStr(name_length)); |
| |
| if (this._rfb_tightvnc) { |
| if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } |
| // In TightVNC mode, ServerInit message is extended |
| const numServerMessages = this._sock.rQshift16(); |
| const numClientMessages = this._sock.rQshift16(); |
| const numEncodings = this._sock.rQshift16(); |
| this._sock.rQskipBytes(2); // padding |
| |
| const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; |
| if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } |
| |
| // we don't actually do anything with the capability information that TIGHT sends, |
| // so we just skip the all of this. |
| |
| // TIGHT server message capabilities |
| this._sock.rQskipBytes(16 * numServerMessages); |
| |
| // TIGHT client message capabilities |
| this._sock.rQskipBytes(16 * numClientMessages); |
| |
| // TIGHT encoding capabilities |
| this._sock.rQskipBytes(16 * numEncodings); |
| } |
| |
| // NB(directxman12): these are down here so that we don't run them multiple times |
| // if we backtrack |
| Log.Info("Screen: " + width + "x" + height + |
| ", bpp: " + bpp + ", depth: " + depth + |
| ", big_endian: " + big_endian + |
| ", true_color: " + true_color + |
| ", red_max: " + red_max + |
| ", green_max: " + green_max + |
| ", blue_max: " + blue_max + |
| ", red_shift: " + red_shift + |
| ", green_shift: " + green_shift + |
| ", blue_shift: " + blue_shift); |
| |
| if (big_endian !== 0) { |
| Log.Warn("Server native endian is not little endian"); |
| } |
| |
| if (red_shift !== 16) { |
| Log.Warn("Server native red-shift is not 16"); |
| } |
| |
| if (blue_shift !== 0) { |
| Log.Warn("Server native blue-shift is not 0"); |
| } |
| |
| this._resize(width, height); |
| |
| if (!this._viewOnly) { this._keyboard.grab(); } |
| if (!this._viewOnly) { this._mouse.grab(); } |
| |
| this._fb_depth = 24; |
| |
| if (this._fb_name === "Intel(r) AMT KVM") { |
| Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode."); |
| this._fb_depth = 8; |
| } |
| |
| RFB.messages.pixelFormat(this._sock, this._fb_depth, true); |
| this._sendEncodings(); |
| RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height); |
| |
| this._updateConnectionState('connected'); |
| return true; |
| } |
| |
| _sendEncodings() { |
| const encs = []; |
| |
| // In preference order |
| encs.push(encodings.encodingCopyRect); |
| // Only supported with full depth support |
| if (this._fb_depth == 24) { |
| encs.push(encodings.encodingTight); |
| encs.push(encodings.encodingTightPNG); |
| encs.push(encodings.encodingHextile); |
| encs.push(encodings.encodingRRE); |
| } |
| encs.push(encodings.encodingRaw); |
| |
| // Psuedo-encoding settings |
| encs.push(encodings.pseudoEncodingQualityLevel0 + 6); |
| encs.push(encodings.pseudoEncodingCompressLevel0 + 2); |
| |
| encs.push(encodings.pseudoEncodingDesktopSize); |
| encs.push(encodings.pseudoEncodingLastRect); |
| encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent); |
| encs.push(encodings.pseudoEncodingExtendedDesktopSize); |
| encs.push(encodings.pseudoEncodingXvp); |
| encs.push(encodings.pseudoEncodingFence); |
| encs.push(encodings.pseudoEncodingContinuousUpdates); |
| |
| if (this._fb_depth == 24) { |
| encs.push(encodings.pseudoEncodingCursor); |
| } |
| |
| RFB.messages.clientEncodings(this._sock, encs); |
| } |
| |
| /* RFB protocol initialization states: |
| * ProtocolVersion |
| * Security |
| * Authentication |
| * SecurityResult |
| * ClientInitialization - not triggered by server message |
| * ServerInitialization |
| */ |
| _init_msg() { |
| switch (this._rfb_init_state) { |
| case 'ProtocolVersion': |
| return this._negotiate_protocol_version(); |
| |
| case 'Security': |
| return this._negotiate_security(); |
| |
| case 'Authentication': |
| return this._negotiate_authentication(); |
| |
| case 'SecurityResult': |
| return this._handle_security_result(); |
| |
| case 'SecurityReason': |
| return this._handle_security_reason(); |
| |
| case 'ClientInitialisation': |
| this._sock.send([0]); // ClientInitialisation for exclusive access |
| this._rfb_init_state = 'ServerInitialisation'; |
| return true; |
| |
| case 'ServerInitialisation': |
| return this._negotiate_server_init(); |
| |
| default: |
| return this._fail("Unknown init state (state: " + |
| this._rfb_init_state + ")"); |
| } |
| } |
| |
| _handle_set_colour_map_msg() { |
| Log.Debug("SetColorMapEntries"); |
| |
| return this._fail("Unexpected SetColorMapEntries message"); |
| } |
| |
| _handle_server_cut_text() { |
| Log.Debug("ServerCutText"); |
| |
| if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; } |
| this._sock.rQskipBytes(3); // Padding |
| const length = this._sock.rQshift32(); |
| if (this._sock.rQwait("ServerCutText", length, 8)) { return false; } |
| |
| const text = this._sock.rQshiftStr(length); |
| |
| if (this._viewOnly) { return true; } |
| |
| this.dispatchEvent(new CustomEvent( |
| "clipboard", |
| { detail: { text: text } })); |
| |
| return true; |
| } |
| |
| _handle_server_fence_msg() { |
| if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; } |
| this._sock.rQskipBytes(3); // Padding |
| let flags = this._sock.rQshift32(); |
| let length = this._sock.rQshift8(); |
| |
| if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; } |
| |
| if (length > 64) { |
| Log.Warn("Bad payload length (" + length + ") in fence response"); |
| length = 64; |
| } |
| |
| const payload = this._sock.rQshiftStr(length); |
| |
| this._supportsFence = true; |
| |
| /* |
| * Fence flags |
| * |
| * (1<<0) - BlockBefore |
| * (1<<1) - BlockAfter |
| * (1<<2) - SyncNext |
| * (1<<31) - Request |
| */ |
| |
| if (!(flags & (1<<31))) { |
| return this._fail("Unexpected fence response"); |
| } |
| |
| // Filter out unsupported flags |
| // FIXME: support syncNext |
| flags &= (1<<0) | (1<<1); |
| |
| // BlockBefore and BlockAfter are automatically handled by |
| // the fact that we process each incoming message |
| // synchronuosly. |
| RFB.messages.clientFence(this._sock, flags, payload); |
| |
| return true; |
| } |
| |
| _handle_xvp_msg() { |
| if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; } |
| this._sock.rQskipBytes(1); // Padding |
| const xvp_ver = this._sock.rQshift8(); |
| const xvp_msg = this._sock.rQshift8(); |
| |
| switch (xvp_msg) { |
| case 0: // XVP_FAIL |
| Log.Error("XVP Operation Failed"); |
| break; |
| case 1: // XVP_INIT |
| this._rfb_xvp_ver = xvp_ver; |
| Log.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")"); |
| this._setCapability("power", true); |
| break; |
| default: |
| this._fail("Illegal server XVP message (msg: " + xvp_msg + ")"); |
| break; |
| } |
| |
| return true; |
| } |
| |
| _normal_msg() { |
| let msg_type; |
| if (this._FBU.rects > 0) { |
| msg_type = 0; |
| } else { |
| msg_type = this._sock.rQshift8(); |
| } |
| |
| let first, ret; |
| switch (msg_type) { |
| case 0: // FramebufferUpdate |
| ret = this._framebufferUpdate(); |
| if (ret && !this._enabledContinuousUpdates) { |
| RFB.messages.fbUpdateRequest(this._sock, true, 0, 0, |
| this._fb_width, this._fb_height); |
| } |
| return ret; |
| |
| case 1: // SetColorMapEntries |
| return this._handle_set_colour_map_msg(); |
| |
| case 2: // Bell |
| Log.Debug("Bell"); |
| this.dispatchEvent(new CustomEvent( |
| "bell", |
| { detail: {} })); |
| return true; |
| |
| case 3: // ServerCutText |
| return this._handle_server_cut_text(); |
| |
| case 150: // EndOfContinuousUpdates |
| first = !this._supportsContinuousUpdates; |
| this._supportsContinuousUpdates = true; |
| this._enabledContinuousUpdates = false; |
| if (first) { |
| this._enabledContinuousUpdates = true; |
| this._updateContinuousUpdates(); |
| Log.Info("Enabling continuous updates."); |
| } else { |
| // FIXME: We need to send a framebufferupdaterequest here |
| // if we add support for turning off continuous updates |
| } |
| return true; |
| |
| case 248: // ServerFence |
| return this._handle_server_fence_msg(); |
| |
| case 250: // XVP |
| return this._handle_xvp_msg(); |
| |
| default: |
| this._fail("Unexpected server message (type " + msg_type + ")"); |
| Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); |
| return true; |
| } |
| } |
| |
| _onFlush() { |
| this._flushing = false; |
| // Resume processing |
| if (this._sock.rQlen > 0) { |
| this._handle_message(); |
| } |
| } |
| |
| _framebufferUpdate() { |
| if (this._FBU.rects === 0) { |
| if (this._sock.rQwait("FBU header", 3, 1)) { return false; } |
| this._sock.rQskipBytes(1); // Padding |
| this._FBU.rects = this._sock.rQshift16(); |
| |
| // Make sure the previous frame is fully rendered first |
| // to avoid building up an excessive queue |
| if (this._display.pending()) { |
| this._flushing = true; |
| this._display.flush(); |
| return false; |
| } |
| } |
| |
| while (this._FBU.rects > 0) { |
| if (this._FBU.encoding === null) { |
| if (this._sock.rQwait("rect header", 12)) { return false; } |
| /* New FramebufferUpdate */ |
| |
| const hdr = this._sock.rQshiftBytes(12); |
| this._FBU.x = (hdr[0] << 8) + hdr[1]; |
| this._FBU.y = (hdr[2] << 8) + hdr[3]; |
| this._FBU.width = (hdr[4] << 8) + hdr[5]; |
| this._FBU.height = (hdr[6] << 8) + hdr[7]; |
| this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + |
| (hdr[10] << 8) + hdr[11], 10); |
| } |
| |
| if (!this._handleRect()) { |
| return false; |
| } |
| |
| this._FBU.rects--; |
| this._FBU.encoding = null; |
| } |
| |
| this._display.flip(); |
| |
| return true; // We finished this FBU |
| } |
| |
| _handleRect() { |
| switch (this._FBU.encoding) { |
| case encodings.pseudoEncodingLastRect: |
| this._FBU.rects = 1; // Will be decreased when we return |
| return true; |
| |
| case encodings.pseudoEncodingCursor: |
| return this._handleCursor(); |
| |
| case encodings.pseudoEncodingQEMUExtendedKeyEvent: |
| // Old Safari doesn't support creating keyboard events |
| try { |
| const keyboardEvent = document.createEvent("keyboardEvent"); |
| if (keyboardEvent.code !== undefined) { |
| this._qemuExtKeyEventSupported = true; |
| } |
| } catch (err) { |
| // Do nothing |
| } |
| return true; |
| |
| case encodings.pseudoEncodingDesktopSize: |
| this._resize(this._FBU.width, this._FBU.height); |
| return true; |
| |
| case encodings.pseudoEncodingExtendedDesktopSize: |
| return this._handleExtendedDesktopSize(); |
| |
| default: |
| return this._handleDataRect(); |
| } |
| } |
| |
| _handleCursor() { |
| const hotx = this._FBU.x; // hotspot-x |
| const hoty = this._FBU.y; // hotspot-y |
| const w = this._FBU.width; |
| const h = this._FBU.height; |
| |
| const pixelslength = w * h * 4; |
| const masklength = Math.ceil(w / 8) * h; |
| |
| let bytes = pixelslength + masklength; |
| if (this._sock.rQwait("cursor encoding", bytes)) { |
| return false; |
| } |
| |
| // Decode from BGRX pixels + bit mask to RGBA |
| const pixels = this._sock.rQshiftBytes(pixelslength); |
| const mask = this._sock.rQshiftBytes(masklength); |
| let rgba = new Uint8Array(w * h * 4); |
| |
| let pix_idx = 0; |
| for (let y = 0; y < h; y++) { |
| for (let x = 0; x < w; x++) { |
| let mask_idx = y * Math.ceil(w / 8) + Math.floor(x / 8); |
| let alpha = (mask[mask_idx] << (x % 8)) & 0x80 ? 255 : 0; |
| rgba[pix_idx ] = pixels[pix_idx + 2]; |
| rgba[pix_idx + 1] = pixels[pix_idx + 1]; |
| rgba[pix_idx + 2] = pixels[pix_idx]; |
| rgba[pix_idx + 3] = alpha; |
| pix_idx += 4; |
| } |
| } |
| |
| this._updateCursor(rgba, hotx, hoty, w, h); |
| |
| return true; |
| } |
| |
| _handleExtendedDesktopSize() { |
| if (this._sock.rQwait("ExtendedDesktopSize", 4)) { |
| return false; |
| } |
| |
| const number_of_screens = this._sock.rQpeek8(); |
| |
| let bytes = 4 + (number_of_screens * 16); |
| if (this._sock.rQwait("ExtendedDesktopSize", bytes)) { |
| return false; |
| } |
| |
| const firstUpdate = !this._supportsSetDesktopSize; |
| this._supportsSetDesktopSize = true; |
| |
| // Normally we only apply the current resize mode after a |
| // window resize event. However there is no such trigger on the |
| // initial connect. And we don't know if the server supports |
| // resizing until we've gotten here. |
| if (firstUpdate) { |
| this._requestRemoteResize(); |
| } |
| |
| this._sock.rQskipBytes(1); // number-of-screens |
| this._sock.rQskipBytes(3); // padding |
| |
| for (let i = 0; i < number_of_screens; i += 1) { |
| // Save the id and flags of the first screen |
| if (i === 0) { |
| this._screen_id = this._sock.rQshiftBytes(4); // id |
| this._sock.rQskipBytes(2); // x-position |
| this._sock.rQskipBytes(2); // y-position |
| this._sock.rQskipBytes(2); // width |
| this._sock.rQskipBytes(2); // height |
| this._screen_flags = this._sock.rQshiftBytes(4); // flags |
| } else { |
| this._sock.rQskipBytes(16); |
| } |
| } |
| |
| /* |
| * The x-position indicates the reason for the change: |
| * |
| * 0 - server resized on its own |
| * 1 - this client requested the resize |
| * 2 - another client requested the resize |
| */ |
| |
| // We need to handle errors when we requested the resize. |
| if (this._FBU.x === 1 && this._FBU.y !== 0) { |
| let msg = ""; |
| // The y-position indicates the status code from the server |
| switch (this._FBU.y) { |
| case 1: |
| msg = "Resize is administratively prohibited"; |
| break; |
| case 2: |
| msg = "Out of resources"; |
| break; |
| case 3: |
| msg = "Invalid screen layout"; |
| break; |
| default: |
| msg = "Unknown reason"; |
| break; |
| } |
| Log.Warn("Server did not accept the resize request: " |
| + msg); |
| } else { |
| this._resize(this._FBU.width, this._FBU.height); |
| } |
| |
| return true; |
| } |
| |
| _handleDataRect() { |
| let decoder = this._decoders[this._FBU.encoding]; |
| if (!decoder) { |
| this._fail("Unsupported encoding (encoding: " + |
| this._FBU.encoding + ")"); |
| return false; |
| } |
| |
| try { |
| return decoder.decodeRect(this._FBU.x, this._FBU.y, |
| this._FBU.width, this._FBU.height, |
| this._sock, this._display, |
| this._fb_depth); |
| } catch (err) { |
| this._fail("Error decoding rect: " + err); |
| return false; |
| } |
| } |
| |
| _updateContinuousUpdates() { |
| if (!this._enabledContinuousUpdates) { return; } |
| |
| RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0, |
| this._fb_width, this._fb_height); |
| } |
| |
| _resize(width, height) { |
| this._fb_width = width; |
| this._fb_height = height; |
| |
| this._display.resize(this._fb_width, this._fb_height); |
| |
| // Adjust the visible viewport based on the new dimensions |
| this._updateClip(); |
| this._updateScale(); |
| |
| this._updateContinuousUpdates(); |
| } |
| |
| _xvpOp(ver, op) { |
| if (this._rfb_xvp_ver < ver) { return; } |
| Log.Info("Sending XVP operation " + op + " (version " + ver + ")"); |
| RFB.messages.xvpOp(this._sock, ver, op); |
| } |
| |
| _updateCursor(rgba, hotx, hoty, w, h) { |
| this._cursorImage = { |
| rgbaPixels: rgba, |
| hotx: hotx, hoty: hoty, w: w, h: h, |
| }; |
| this._refreshCursor(); |
| } |
| |
| _shouldShowDotCursor() { |
| // Called when this._cursorImage is updated |
| if (!this._showDotCursor) { |
| // User does not want to see the dot, so... |
| return false; |
| } |
| |
| // The dot should not be shown if the cursor is already visible, |
| // i.e. contains at least one not-fully-transparent pixel. |
| // So iterate through all alpha bytes in rgba and stop at the |
| // first non-zero. |
| for (let i = 3; i < this._cursorImage.rgbaPixels.length; i += 4) { |
| if (this._cursorImage.rgbaPixels[i]) { |
| return false; |
| } |
| } |
| |
| // At this point, we know that the cursor is fully transparent, and |
| // the user wants to see the dot instead of this. |
| return true; |
| } |
| |
| _refreshCursor() { |
| const image = this._shouldShowDotCursor() ? RFB.cursors.dot : this._cursorImage; |
| this._cursor.change(image.rgbaPixels, |
| image.hotx, image.hoty, |
| image.w, image.h |
| ); |
| } |
| |
| static genDES(password, challenge) { |
| const passwordChars = password.split('').map(c => c.charCodeAt(0)); |
| return (new DES(passwordChars)).encrypt(challenge); |
| } |
| } |
| |
| // Class Methods |
| RFB.messages = { |
| keyEvent(sock, keysym, down) { |
| const buff = sock._sQ; |
| const offset = sock._sQlen; |
| |
| buff[offset] = 4; // msg-type |
| buff[offset + 1] = down; |
| |
| buff[offset + 2] = 0; |
| buff[offset + 3] = 0; |
| |
| buff[offset + 4] = (keysym >> 24); |
| buff[offset + 5] = (keysym >> 16); |
| buff[offset + 6] = (keysym >> 8); |
| buff[offset + 7] = keysym; |
| |
| sock._sQlen += 8; |
| sock.flush(); |
| }, |
| |
| QEMUExtendedKeyEvent(sock, keysym, down, keycode) { |
| function getRFBkeycode(xt_scancode) { |
| const upperByte = (keycode >> 8); |
| const lowerByte = (keycode & 0x00ff); |
| if (upperByte === 0xe0 && lowerByte < 0x7f) { |
| return lowerByte | 0x80; |
| } |
| return xt_scancode; |
| } |
| |
| const buff = sock._sQ; |
| const offset = sock._sQlen; |
| |
| buff[offset] = 255; // msg-type |
| buff[offset + 1] = 0; // sub msg-type |
| |
| buff[offset + 2] = (down >> 8); |
| buff[offset + 3] = down; |
| |
| buff[offset + 4] = (keysym >> 24); |
| buff[offset + 5] = (keysym >> 16); |
| buff[offset + 6] = (keysym >> 8); |
| buff[offset + 7] = keysym; |
| |
| const RFBkeycode = getRFBkeycode(keycode); |
| |
| buff[offset + 8] = (RFBkeycode >> 24); |
| buff[offset + 9] = (RFBkeycode >> 16); |
| buff[offset + 10] = (RFBkeycode >> 8); |
| buff[offset + 11] = RFBkeycode; |
| |
| sock._sQlen += 12; |
| sock.flush(); |
| }, |
| |
| pointerEvent(sock, x, y, mask) { |
| const buff = sock._sQ; |
| const offset = sock._sQlen; |
| |
| buff[offset] = 5; // msg-type |
| |
| buff[offset + 1] = mask; |
| |
| buff[offset + 2] = x >> 8; |
| buff[offset + 3] = x; |
| |
| buff[offset + 4] = y >> 8; |
| buff[offset + 5] = y; |
| |
| sock._sQlen += 6; |
| sock.flush(); |
| }, |
| |
| // TODO(directxman12): make this unicode compatible? |
| clientCutText(sock, text) { |
| const buff = sock._sQ; |
| const offset = sock._sQlen; |
| |
| buff[offset] = 6; // msg-type |
| |
| buff[offset + 1] = 0; // padding |
| buff[offset + 2] = 0; // padding |
| buff[offset + 3] = 0; // padding |
| |
| let length = text.length; |
| |
| buff[offset + 4] = length >> 24; |
| buff[offset + 5] = length >> 16; |
| buff[offset + 6] = length >> 8; |
| buff[offset + 7] = length; |
| |
| sock._sQlen += 8; |
| |
| // We have to keep track of from where in the text we begin creating the |
| // buffer for the flush in the next iteration. |
| let textOffset = 0; |
| |
| let remaining = length; |
| while (remaining > 0) { |
| |
| let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen)); |
| for (let i = 0; i < flushSize; i++) { |
| buff[sock._sQlen + i] = text.charCodeAt(textOffset + i); |
| } |
| |
| sock._sQlen += flushSize; |
| sock.flush(); |
| |
| remaining -= flushSize; |
| textOffset += flushSize; |
| } |
| }, |
| |
| setDesktopSize(sock, width, height, id, flags) { |
| const buff = sock._sQ; |
| const offset = sock._sQlen; |
| |
| buff[offset] = 251; // msg-type |
| buff[offset + 1] = 0; // padding |
| buff[offset + 2] = width >> 8; // width |
| buff[offset + 3] = width; |
| buff[offset + 4] = height >> 8; // height |
| buff[offset + 5] = height; |
| |
| buff[offset + 6] = 1; // number-of-screens |
| buff[offset + 7] = 0; // padding |
| |
| // screen array |
| buff[offset + 8] = id >> 24; // id |
| buff[offset + 9] = id >> 16; |
| buff[offset + 10] = id >> 8; |
| buff[offset + 11] = id; |
| buff[offset + 12] = 0; // x-position |
| buff[offset + 13] = 0; |
| buff[offset + 14] = 0; // y-position |
| buff[offset + 15] = 0; |
| buff[offset + 16] = width >> 8; // width |
| buff[offset + 17] = width; |
| buff[offset + 18] = height >> 8; // height |
| buff[offset + 19] = height; |
| buff[offset + 20] = flags >> 24; // flags |
| buff[offset + 21] = flags >> 16; |
| buff[offset + 22] = flags >> 8; |
| buff[offset + 23] = flags; |
| |
| sock._sQlen += 24; |
| sock.flush(); |
| }, |
| |
| clientFence(sock, flags, payload) { |
| const buff = sock._sQ; |
| const offset = sock._sQlen; |
| |
| buff[offset] = 248; // msg-type |
| |
| buff[offset + 1] = 0; // padding |
| buff[offset + 2] = 0; // padding |
| buff[offset + 3] = 0; // padding |
| |
| buff[offset + 4] = flags >> 24; // flags |
| buff[offset + 5] = flags >> 16; |
| buff[offset + 6] = flags >> 8; |
| buff[offset + 7] = flags; |
| |
| const n = payload.length; |
| |
| buff[offset + 8] = n; // length |
| |
| for (let i = 0; i < n; i++) { |
| buff[offset + 9 + i] = payload.charCodeAt(i); |
| } |
| |
| sock._sQlen += 9 + n; |
| sock.flush(); |
| }, |
| |
| enableContinuousUpdates(sock, enable, x, y, width, height) { |
| const buff = sock._sQ; |
| const offset = sock._sQlen; |
| |
| buff[offset] = 150; // msg-type |
| buff[offset + 1] = enable; // enable-flag |
| |
| buff[offset + 2] = x >> 8; // x |
| buff[offset + 3] = x; |
| buff[offset + 4] = y >> 8; // y |
| buff[offset + 5] = y; |
| buff[offset + 6] = width >> 8; // width |
| buff[offset + 7] = width; |
| buff[offset + 8] = height >> 8; // height |
| buff[offset + 9] = height; |
| |
| sock._sQlen += 10; |
| sock.flush(); |
| }, |
| |
| pixelFormat(sock, depth, true_color) { |
| const buff = sock._sQ; |
| const offset = sock._sQlen; |
| |
| let bpp; |
| |
| if (depth > 16) { |
| bpp = 32; |
| } else if (depth > 8) { |
| bpp = 16; |
| } else { |
| bpp = 8; |
| } |
| |
| const bits = Math.floor(depth/3); |
| |
| buff[offset] = 0; // msg-type |
| |
| buff[offset + 1] = 0; // padding |
| buff[offset + 2] = 0; // padding |
| buff[offset + 3] = 0; // padding |
| |
| buff[offset + 4] = bpp; // bits-per-pixel |
| buff[offset + 5] = depth; // depth |
| buff[offset + 6] = 0; // little-endian |
| buff[offset + 7] = true_color ? 1 : 0; // true-color |
| |
| buff[offset + 8] = 0; // red-max |
| buff[offset + 9] = (1 << bits) - 1; // red-max |
| |
| buff[offset + 10] = 0; // green-max |
| buff[offset + 11] = (1 << bits) - 1; // green-max |
| |
| buff[offset + 12] = 0; // blue-max |
| buff[offset + 13] = (1 << bits) - 1; // blue-max |
| |
| buff[offset + 14] = bits * 2; // red-shift |
| buff[offset + 15] = bits * 1; // green-shift |
| buff[offset + 16] = bits * 0; // blue-shift |
| |
| buff[offset + 17] = 0; // padding |
| buff[offset + 18] = 0; // padding |
| buff[offset + 19] = 0; // padding |
| |
| sock._sQlen += 20; |
| sock.flush(); |
| }, |
| |
| clientEncodings(sock, encodings) { |
| const buff = sock._sQ; |
| const offset = sock._sQlen; |
| |
| buff[offset] = 2; // msg-type |
| buff[offset + 1] = 0; // padding |
| |
| buff[offset + 2] = encodings.length >> 8; |
| buff[offset + 3] = encodings.length; |
| |
| let j = offset + 4; |
| for (let i = 0; i < encodings.length; i++) { |
| const enc = encodings[i]; |
| buff[j] = enc >> 24; |
| buff[j + 1] = enc >> 16; |
| buff[j + 2] = enc >> 8; |
| buff[j + 3] = enc; |
| |
| j += 4; |
| } |
| |
| sock._sQlen += j - offset; |
| sock.flush(); |
| }, |
| |
| fbUpdateRequest(sock, incremental, x, y, w, h) { |
| const buff = sock._sQ; |
| const offset = sock._sQlen; |
| |
| if (typeof(x) === "undefined") { x = 0; } |
| if (typeof(y) === "undefined") { y = 0; } |
| |
| buff[offset] = 3; // msg-type |
| buff[offset + 1] = incremental ? 1 : 0; |
| |
| buff[offset + 2] = (x >> 8) & 0xFF; |
| buff[offset + 3] = x & 0xFF; |
| |
| buff[offset + 4] = (y >> 8) & 0xFF; |
| buff[offset + 5] = y & 0xFF; |
| |
| buff[offset + 6] = (w >> 8) & 0xFF; |
| buff[offset + 7] = w & 0xFF; |
| |
| buff[offset + 8] = (h >> 8) & 0xFF; |
| buff[offset + 9] = h & 0xFF; |
| |
| sock._sQlen += 10; |
| sock.flush(); |
| }, |
| |
| xvpOp(sock, ver, op) { |
| const buff = sock._sQ; |
| const offset = sock._sQlen; |
| |
| buff[offset] = 250; // msg-type |
| buff[offset + 1] = 0; // padding |
| |
| buff[offset + 2] = ver; |
| buff[offset + 3] = op; |
| |
| sock._sQlen += 4; |
| sock.flush(); |
| } |
| }; |
| |
| RFB.cursors = { |
| none: { |
| rgbaPixels: new Uint8Array(), |
| w: 0, h: 0, |
| hotx: 0, hoty: 0, |
| }, |
| |
| dot: { |
| /* eslint-disable indent */ |
| rgbaPixels: new Uint8Array([ |
| 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, |
| 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 255, |
| 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, |
| ]), |
| /* eslint-enable indent */ |
| w: 3, h: 3, |
| hotx: 1, hoty: 1, |
| } |
| }; |