From: Pierre Ossman Date: Fri, 10 Nov 2017 13:19:05 +0000 (+0100) Subject: Merge branch 'api' of https://github.com/CendioOssman/noVNC X-Git-Tag: v1.0.0~43 X-Git-Url: https://git.proxmox.com/?a=commitdiff_plain;h=a201bfc5eb781f8cb3dd76558686cddab23aa86e;hp=-c;p=mirror_novnc.git Merge branch 'api' of https://github.com/CendioOssman/noVNC --- a201bfc5eb781f8cb3dd76558686cddab23aa86e diff --combined app/ui.js index da912f3,74735cc..f661567 --- a/app/ui.js +++ b/app/ui.js @@@ -13,7 -13,7 +13,7 @@@ import * as Log from '../core/util/logging.js'; import _, { l10n } from '../core/util/localization.js'; - import { isTouchDevice, browserSupportsCursorURIs as cursorURIsSupported } from '../core/util/browsers.js'; + import { isTouchDevice } from '../core/util/browsers.js'; import { setCapture, getPointerEvent } from '../core/util/events.js'; import KeyTable from "../core/input/keysym.js"; import keysyms from "../core/input/keysymdef.js"; @@@ -91,7 -91,7 +91,7 @@@ var UI = UI.addControlbarHandlers(); UI.addTouchSpecificHandlers(); UI.addExtraKeysHandlers(); - UI.addXvpHandlers(); + UI.addMachineHandlers(); UI.addConnectionControlHandlers(); UI.addClipboardHandlers(); UI.addSettingsHandlers(); @@@ -167,7 -167,6 +167,6 @@@ UI.initSetting('host', window.location.hostname); UI.initSetting('port', port); UI.initSetting('encrypt', (window.location.protocol === "https:")); - UI.initSetting('cursor', !isTouchDevice); UI.initSetting('view_clip', false); UI.initSetting('resize', 'off'); UI.initSetting('shared', true); @@@ -200,28 -199,6 +199,6 @@@ } }, - initRFB: function() { - try { - UI.rfb = new RFB({'target': document.getElementById('noVNC_canvas'), - 'onNotification': UI.notification, - 'onUpdateState': UI.updateState, - 'onDisconnected': UI.disconnectFinished, - 'onPasswordRequired': UI.passwordRequired, - 'onXvpInit': UI.updateXvpButton, - 'onClipboard': UI.clipboardReceive, - 'onBell': UI.bell, - 'onFBUComplete': UI.initialResize, - 'onFBResize': UI.updateSessionSize, - 'onDesktopName': UI.updateDesktopName}); - return true; - } catch (exc) { - var msg = "Unable to create RFB client -- " + exc; - Log.Error(msg); - UI.showStatus(msg, 'error'); - return false; - } - }, - /* ------^------- * /INIT * ============== @@@ -278,8 -255,8 +255,8 @@@ document.getElementById("noVNC_keyboard_button") .addEventListener('click', UI.toggleVirtualKeyboard); - UI.touchKeyboard = new Keyboard({target: document.getElementById('noVNC_keyboardinput'), - onKeyEvent: UI.keyEvent}); + UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput')); + UI.touchKeyboard.onkeyevent = UI.keyEvent; UI.touchKeyboard.grab(); document.getElementById("noVNC_keyboardinput") .addEventListener('input', UI.keyInput); @@@ -292,6 -269,8 +269,6 @@@ document.documentElement .addEventListener('mousedown', UI.keepVirtualKeyboard, true); - document.documentElement - .addEventListener('touchstart', UI.keepVirtualKeyboard, true); document.getElementById("noVNC_control_bar") .addEventListener('touchstart', UI.activateControlbar); @@@ -330,15 -309,15 +307,15 @@@ .addEventListener('click', UI.sendCtrlAltDel); }, - addXvpHandlers: function() { - document.getElementById("noVNC_xvp_shutdown_button") - .addEventListener('click', function() { UI.rfb.xvpShutdown(); }); - document.getElementById("noVNC_xvp_reboot_button") - .addEventListener('click', function() { UI.rfb.xvpReboot(); }); - document.getElementById("noVNC_xvp_reset_button") - .addEventListener('click', function() { UI.rfb.xvpReset(); }); - document.getElementById("noVNC_xvp_button") - .addEventListener('click', UI.toggleXvpPanel); + addMachineHandlers: function() { + document.getElementById("noVNC_shutdown_button") + .addEventListener('click', function() { UI.rfb.machineShutdown(); }); + document.getElementById("noVNC_reboot_button") + .addEventListener('click', function() { UI.rfb.machineReboot(); }); + document.getElementById("noVNC_reset_button") + .addEventListener('click', function() { UI.rfb.machineReset(); }); + document.getElementById("noVNC_power_button") + .addEventListener('click', UI.togglePowerPanel); }, addConnectionControlHandlers: function() { @@@ -377,8 -356,6 +354,6 @@@ .addEventListener('click', UI.toggleSettingsPanel); UI.addSettingChangeHandler('encrypt'); - UI.addSettingChangeHandler('cursor'); - UI.addSettingChangeHandler('cursor', UI.updateLocalCursor); UI.addSettingChangeHandler('resize'); UI.addSettingChangeHandler('resize', UI.enableDisableViewClip); UI.addSettingChangeHandler('resize', UI.applyResizeMode); @@@ -413,7 -390,7 +388,7 @@@ * VISUAL * ------v------*/ - updateState: function(rfb, state, oldstate) { + updateState: function(event) { var msg; document.documentElement.classList.remove("noVNC_connecting"); @@@ -421,7 -398,7 +396,7 @@@ document.documentElement.classList.remove("noVNC_disconnecting"); document.documentElement.classList.remove("noVNC_reconnecting"); - switch (state) { + switch (event.detail.state) { case 'connecting': document.getElementById("noVNC_transition_text").textContent = _("Connecting..."); document.documentElement.classList.add("noVNC_connecting"); @@@ -429,8 -406,9 +404,9 @@@ case 'connected': UI.connected = true; UI.inhibit_reconnect = false; + UI.doneInitialResize = false; document.documentElement.classList.add("noVNC_connected"); - if (rfb && rfb.get_encrypt()) { + if (UI.getSetting('encrypt')) { msg = _("Connected (encrypted) to ") + UI.desktopName; } else { msg = _("Connected (unencrypted) to ") + UI.desktopName; @@@ -462,12 -440,6 +438,6 @@@ UI.enableDisableViewClip(); - if (cursorURIsSupported() && !isTouchDevice) { - UI.enableSetting('cursor'); - } else { - UI.disableSetting('cursor'); - } - if (UI.connected) { UI.disableSetting('encrypt'); UI.disableSetting('shared'); @@@ -487,12 -459,12 +457,12 @@@ UI.enableSetting('port'); UI.enableSetting('path'); UI.enableSetting('repeaterID'); - UI.updateXvpButton(0); + UI.updatePowerButton(); UI.keepControlbar(); } // Hide input related buttons in view only mode - if (UI.rfb && UI.rfb.get_view_only()) { + if (UI.rfb && UI.rfb.viewOnly) { document.getElementById('noVNC_keyboard_button') .classList.add('noVNC_hidden'); document.getElementById('noVNC_toggle_extra_keys_button') @@@ -562,8 -534,8 +532,8 @@@ document.getElementById('noVNC_status').classList.remove("noVNC_open"); }, - notification: function (rfb, msg, level, options) { - UI.showStatus(msg, level); + notification: function (e) { + UI.showStatus(e.detail.message, e.detail.level); }, activateControlbar: function(event) { @@@ -866,7 -838,7 +836,7 @@@ closeAllPanels: function() { UI.closeSettingsPanel(); - UI.closeXvpPanel(); + UI.closePowerPanel(); UI.closeClipboardPanel(); UI.closeExtraKeys(); }, @@@ -883,12 -855,6 +853,6 @@@ // Refresh UI elements from saved cookies UI.updateSetting('encrypt'); - if (cursorURIsSupported()) { - UI.updateSetting('cursor'); - } else { - UI.updateSetting('cursor', !isTouchDevice); - UI.disableSetting('cursor'); - } UI.updateSetting('view_clip'); UI.updateSetting('resize'); UI.updateSetting('shared'); @@@ -924,50 -890,52 +888,52 @@@ /* ------^------- * /SETTINGS * ============== - * XVP + * POWER * ------v------*/ - openXvpPanel: function() { + openPowerPanel: function() { UI.closeAllPanels(); UI.openControlbar(); - document.getElementById('noVNC_xvp') + document.getElementById('noVNC_power') .classList.add("noVNC_open"); - document.getElementById('noVNC_xvp_button') + document.getElementById('noVNC_power_button') .classList.add("noVNC_selected"); }, - closeXvpPanel: function() { - document.getElementById('noVNC_xvp') + closePowerPanel: function() { + document.getElementById('noVNC_power') .classList.remove("noVNC_open"); - document.getElementById('noVNC_xvp_button') + document.getElementById('noVNC_power_button') .classList.remove("noVNC_selected"); }, - toggleXvpPanel: function() { - if (document.getElementById('noVNC_xvp') + togglePowerPanel: function() { + if (document.getElementById('noVNC_power') .classList.contains("noVNC_open")) { - UI.closeXvpPanel(); + UI.closePowerPanel(); } else { - UI.openXvpPanel(); + UI.openPowerPanel(); } }, - // Disable/enable XVP button - updateXvpButton: function(ver) { - if (ver >= 1 && !UI.rfb.get_view_only()) { - document.getElementById('noVNC_xvp_button') + // Disable/enable power button + updatePowerButton: function() { + if (UI.connected && + UI.rfb.capabilities.power && + !UI.rfb.viewOnly) { + document.getElementById('noVNC_power_button') .classList.remove("noVNC_hidden"); } else { - document.getElementById('noVNC_xvp_button') + document.getElementById('noVNC_power_button') .classList.add("noVNC_hidden"); - // Close XVP panel if open - UI.closeXvpPanel(); + // Close power panel if open + UI.closePowerPanel(); } }, /* ------^------- - * /XVP + * /POWER * ============== * CLIPBOARD * ------v------*/ @@@ -998,9 -966,9 +964,9 @@@ } }, - clipboardReceive: function(rfb, text) { - Log.Debug(">> UI.clipboardReceive: " + text.substr(0,40) + "..."); - document.getElementById('noVNC_clipboard_text').value = text; + clipboardReceive: function(e) { + Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0,40) + "..."); + document.getElementById('noVNC_clipboard_text').value = e.detail.text; Log.Debug("<< UI.clipboardReceive"); }, @@@ -1053,19 -1021,34 +1019,34 @@@ return; } - if (!UI.initRFB()) return; - UI.closeAllPanels(); UI.closeConnectPanel(); - UI.rfb.set_encrypt(UI.getSetting('encrypt')); - UI.rfb.set_shared(UI.getSetting('shared')); - UI.rfb.set_repeaterID(UI.getSetting('repeaterID')); - - UI.updateLocalCursor(); UI.updateViewOnly(); - UI.rfb.connect(host, port, password, path); + var url; + + url = UI.getSetting('encrypt') ? 'wss' : 'ws'; + + url += '://' + host; + if(port) { + url += ':' + port; + } + url += '/' + path; + + UI.rfb = new RFB(document.getElementById('noVNC_canvas'), url, + { shared: UI.getSetting('shared'), + repeaterID: UI.getSetting('repeaterID'), + credentials: { password: password } }); + UI.rfb.addEventListener("notification", UI.notification); + UI.rfb.addEventListener("updatestate", UI.updateState); + UI.rfb.addEventListener("disconnect", UI.disconnectFinished); + UI.rfb.addEventListener("credentialsrequired", UI.credentials); + UI.rfb.addEventListener("capabilities", function () { UI.updatePowerButton(); UI.initialResize(); }); + UI.rfb.addEventListener("clipboard", UI.clipboardReceive); + UI.rfb.addEventListener("bell", UI.bell); + UI.rfb.addEventListener("fbresize", UI.updateSessionSize); + UI.rfb.addEventListener("desktopname", UI.updateDesktopName); }, disconnect: function() { @@@ -1075,9 -1058,6 +1056,6 @@@ // Disable automatic reconnecting UI.inhibit_reconnect = true; - // Restore the callback used for initial resize - UI.rfb.set_onFBUComplete(UI.initialResize); - // Don't display the connection settings until we're actually disconnected }, @@@ -1092,9 -1072,9 +1070,9 @@@ UI.connect(null, UI.reconnect_password); }, - disconnectFinished: function (rfb, reason) { - if (typeof reason !== 'undefined') { - UI.showStatus(reason, 'error'); + disconnectFinished: function (e) { + if (typeof e.detail.reason !== 'undefined') { + UI.showStatus(e.detail.reason, 'error'); } else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) { document.getElementById("noVNC_transition_text").textContent = _("Reconnecting..."); document.documentElement.classList.add("noVNC_reconnecting"); @@@ -1125,8 -1105,8 +1103,8 @@@ * PASSWORD * ------v------*/ - passwordRequired: function(rfb, msg) { - + credentials: function(e) { + // FIXME: handle more types document.getElementById('noVNC_password_dlg') .classList.add('noVNC_open'); @@@ -1134,24 -1114,23 +1112,23 @@@ document.getElementById('noVNC_password_input').focus(); }, 100); - if (typeof msg === 'undefined') { - msg = _("Password is required"); - } + var msg = _("Password is required"); Log.Warn(msg); UI.showStatus(msg, "warning"); }, setPassword: function(e) { + // Prevent actually submitting the form + e.preventDefault(); + var inputElem = document.getElementById('noVNC_password_input'); var password = inputElem.value; // Clear the input after reading the password inputElem.value = ""; - UI.rfb.sendPassword(password); + UI.rfb.sendCredentials({ password: password }); UI.reconnect_password = password; document.getElementById('noVNC_password_dlg') .classList.remove('noVNC_open'); - // Prevent actually submitting the form - e.preventDefault(); }, /* ------^------- @@@ -1214,11 -1193,10 +1191,10 @@@ var screen = UI.screenSize(); - if (screen && UI.connected && UI.rfb.get_display()) { + if (screen && UI.connected) { - var display = UI.rfb.get_display(); var resizeMode = UI.getSetting('resize'); - display.set_scale(1); + UI.rfb.viewportScale = 1.0; // Make sure the viewport is adjusted first UI.updateViewClip(); @@@ -1250,19 -1228,17 +1226,17 @@@ if (!UI.rfb) return; var resizeMode = UI.getSetting('resize'); - if (resizeMode !== 'scale' && resizeMode !== 'downscale') { + if (resizeMode !== 'scale') { return; } var screen = UI.screenSize(); - if (!screen || !UI.connected || !UI.rfb.get_display()) { + if (!screen || !UI.connected) { return; } - var display = UI.rfb.get_display(); - var downscaleOnly = resizeMode === 'downscale'; - display.autoscale(screen.w, screen.h, downscaleOnly); + UI.rfb.autoscale(screen.w, screen.h); UI.fixScrollbars(); }, @@@ -1275,13 -1251,14 +1249,14 @@@ // Normally we only apply the current resize mode after a window resize // event. This means that when a new connection is opened, there is no // resize mode active. - // We have to wait until the first FBU because this is where the client - // will find the supported encodings of the server. Some calls later in - // the chain is dependant on knowing the server-capabilities. - initialResize: function(rfb, fbu) { + // We have to wait until we know the capabilities of the server as + // some calls later in the chain is dependant on knowing the + // server-capabilities. + initialResize: function() { + if (UI.doneInitialResize) return; + UI.applyResizeMode(); - // After doing this once, we remove the callback. - UI.rfb.set_onFBUComplete(function() { }); + UI.doneInitialResize = true; }, /* ------^------- @@@ -1300,12 -1277,11 +1275,11 @@@ updateViewClip: function() { if (!UI.rfb) return; - var display = UI.rfb.get_display(); - var cur_clip = display.get_viewport(); + var cur_clip = UI.rfb.clipViewport; var new_clip = UI.getSetting('view_clip'); var resizeSetting = UI.getSetting('resize'); - if (resizeSetting === 'downscale' || resizeSetting === 'scale') { + if (resizeSetting === 'scale') { // Disable viewport clipping if we are scaling new_clip = false; } else if (isTouchDevice) { @@@ -1314,7 -1290,7 +1288,7 @@@ } if (cur_clip !== new_clip) { - display.set_viewport(new_clip); + UI.rfb.clipViewport = new_clip; } var size = UI.screenSize(); @@@ -1322,7 -1298,7 +1296,7 @@@ if (new_clip && size) { // When clipping is enabled, the screen is limited to // the size of the browser window. - display.viewportChangeSize(size.w, size.h); + UI.rfb.viewportChangeSize(size.w, size.h); UI.fixScrollbars(); } @@@ -1335,7 -1311,7 +1309,7 @@@ enableDisableViewClip: function() { var resizeSetting = UI.getSetting('resize'); // Disable clipping if we are scaling, connected or on touch - if (resizeSetting === 'downscale' || resizeSetting === 'scale' || + if (resizeSetting === 'scale' || isTouchDevice) { UI.disableSetting('view_clip'); } else { @@@ -1352,7 -1328,7 +1326,7 @@@ toggleViewDrag: function() { if (!UI.rfb) return; - var drag = UI.rfb.get_viewportDrag(); + var drag = UI.rfb.dragViewport; UI.setViewDrag(!drag); }, @@@ -1360,7 -1336,7 +1334,7 @@@ setViewDrag: function(drag) { if (!UI.rfb) return; - UI.rfb.set_viewportDrag(drag); + UI.rfb.dragViewport = drag; UI.updateViewDrag(); }, @@@ -1372,22 -1348,21 +1346,21 @@@ // Check if viewport drag is possible. It is only possible // if the remote display is clipping the client display. - if (UI.rfb.get_display().get_viewport() && - UI.rfb.get_display().clippingDisplay()) { + if (UI.rfb.clipViewport && UI.rfb.isClipped) { clipping = true; } var viewDragButton = document.getElementById('noVNC_view_drag_button'); if (!clipping && - UI.rfb.get_viewportDrag()) { + UI.rfb.dragViewport) { // The size of the remote display is the same or smaller // than the client display. Make sure viewport drag isn't // active when it can't be used. - UI.rfb.set_viewportDrag(false); + UI.rfb.dragViewport = false; } - if (UI.rfb.get_viewportDrag()) { + if (UI.rfb.dragViewport) { viewDragButton.classList.add("noVNC_selected"); } else { viewDragButton.classList.remove("noVNC_selected"); @@@ -1458,17 -1433,11 +1431,17 @@@ onfocusVirtualKeyboard: function(event) { document.getElementById('noVNC_keyboard_button') .classList.add("noVNC_selected"); + if (UI.rfb) { + UI.rfb.set_focus_on_click(false); + } }, onblurVirtualKeyboard: function(event) { document.getElementById('noVNC_keyboard_button') .classList.remove("noVNC_selected"); + if (UI.rfb) { + UI.rfb.set_focus_on_click(true); + } }, keepVirtualKeyboard: function(event) { @@@ -1496,7 -1465,14 +1469,7 @@@ } } - // The default action of touchstart is to generate other - // events, which other elements might depend on. So we can't - // blindly prevent that. Instead restore focus right away. - if (event.type === "touchstart") { - setTimeout(input.focus.bind(input)); - } else { - event.preventDefault(); - } + event.preventDefault(); }, keyboardinputReset: function() { @@@ -1655,9 -1631,9 +1628,9 @@@ * ------v------*/ setMouseButton: function(num) { - var view_only = UI.rfb.get_view_only(); + var view_only = UI.rfb.viewOnly; if (UI.rfb && !view_only) { - UI.rfb.get_mouse().set_touchButton(num); + UI.rfb.touchButton = num; } var blist = [0, 1,2,4]; @@@ -1672,21 -1648,16 +1645,16 @@@ } }, - updateLocalCursor: function() { - if (!UI.rfb) return; - UI.rfb.set_local_cursor(UI.getSetting('cursor')); - }, - updateViewOnly: function() { if (!UI.rfb) return; - UI.rfb.set_view_only(UI.getSetting('view_only')); + UI.rfb.viewOnly = UI.getSetting('view_only'); }, updateLogging: function() { WebUtil.init_logging(UI.getSetting('logging')); }, - updateSessionSize: function(rfb, width, height) { + updateSessionSize: function(e) { UI.updateViewClip(); UI.updateScaling(); UI.fixScrollbars(); @@@ -1704,13 -1675,13 +1672,13 @@@ screen.style.overflow = ""; }, - updateDesktopName: function(rfb, name) { - UI.desktopName = name; + updateDesktopName: function(e) { + UI.desktopName = e.detail.name; // Display the desktop name in the document title - document.title = name + " - noVNC"; + document.title = e.detail.name + " - noVNC"; }, - bell: function(rfb) { + bell: function(e) { if (WebUtil.getConfigVar('bell', 'on') === 'on') { var promise = document.getElementById('noVNC_bell').play(); // The standards disagree on the return value here diff --combined core/rfb.js index a16093b,a3546b0..3d12ae4 --- a/core/rfb.js +++ b/core/rfb.js @@@ -13,7 -13,8 +13,8 @@@ import * as Log from './util/logging.js'; import _ from './util/localization.js'; import { decodeUTF8 } from './util/strings.js'; - import { set_defaults, make_properties } from './util/properties.js'; + import { browserSupportsCursorURIs, isTouchDevice } from './util/browsers.js'; + import EventTargetMixin from './util/eventtarget.js'; import Display from "./display.js"; import Keyboard from "./input/keyboard.js"; import Mouse from "./input/mouse.js"; @@@ -24,50 -25,78 +25,78 @@@ import KeyTable from "./input/keysym.js import XtScancode from "./input/xtscancodes.js"; import Inflator from "./inflator.js"; import { encodings, encodingName } from "./encodings.js"; + import "./util/polyfill.js"; /*jslint white: false, browser: true */ /*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES, KeyTable, Inflator, XtScancode */ - export default function RFB(defaults) { - "use strict"; - if (!defaults) { - defaults = {}; + // How many seconds to wait for a disconnect to finish + var DISCONNECT_TIMEOUT = 3; + + export default function RFB(target, url, options) { + if (!target) { + throw Error("Must specify target"); + } + if (!url) { + throw Error("Must specify URL"); } - this._rfb_host = ''; - this._rfb_port = 5900; - this._rfb_password = ''; - this._rfb_path = ''; + this._target = target; + this._url = url; + + // Connection details + options = options || {} + this._rfb_credentials = options.credentials || {}; + this._shared = 'shared' in options ? !!options.shared : true; + this._repeaterID = options.repeaterID || ''; + // Internal state this._rfb_connection_state = ''; this._rfb_init_state = ''; - this._rfb_version = 0; - this._rfb_max_version = 3.8; this._rfb_auth_scheme = ''; this._rfb_disconnect_reason = ""; + // Server capabilities + this._rfb_version = 0; + this._rfb_max_version = 3.8; this._rfb_tightvnc = false; this._rfb_xvp_ver = 0; - this._encHandlers = {}; - this._encStats = {}; + this._fb_width = 0; + this._fb_height = 0; + + this._fb_name = ""; + this._capabilities = { power: false, resize: 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 - this._disconnTimer = null; // disconnection timer - this._supportsFence = false; + // Timers + this._disconnTimer = null; // disconnection timer - this._supportsContinuousUpdates = false; - this._enabledContinuousUpdates = false; + // Decoder states and stats + this._encHandlers = {}; + this._encStats = {}; - // Frame buffer update state this._FBU = { rects: 0, - subrects: 0, // RRE + subrects: 0, // RRE and HEXTILE lines: 0, // RAW tiles: 0, // HEXTILE bytes: 0, @@@ -78,12 -107,11 +107,11 @@@ encoding: 0, subencoding: -1, background: null, - zlib: [] // TIGHT zlib streams + zlibs: [] // TIGHT zlib streams }; - - this._fb_width = 0; - this._fb_height = 0; - this._fb_name = ""; + for (var i = 0; i < 4; i++) { + this._FBU.zlibs[i] = new Inflator(); + } this._destBuff = null; this._paletteBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) @@@ -103,10 -131,6 +131,6 @@@ pixels: 0 }; - this._supportsSetDesktopSize = false; - this._screen_id = 0; - this._screen_flags = 0; - // Mouse state this._mouse_buttonMask = 0; this._mouse_arr = []; @@@ -114,42 -138,6 +138,11 @@@ this._viewportDragPos = {}; this._viewportHasMoved = false; - // QEMU Extended Key Event support - default to false - this._qemuExtKeyEventSupported = false; - - // set the default value on user-facing properties - set_defaults(this, defaults, { - 'target': 'null', // VNC display rendering Canvas object - 'encrypt': false, // Use TLS/SSL/wss encryption - 'local_cursor': false, // Request locally rendered cursor - 'shared': true, // Request shared mode - 'view_only': false, // Disable client mouse/keyboard - 'focus_on_click': true, // Grab focus on canvas on mouse click - 'xvp_password_sep': '@', // Separator for XVP password fields - 'disconnectTimeout': 3, // Time (s) to wait for disconnection - 'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection - 'repeaterID': '', // [UltraVNC] RepeaterID to connect to - 'viewportDrag': false, // Move the viewport on mouse drags - - // Callback functions - 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate): connection state change - 'onNotification': function () { }, // onNotification(rfb, msg, level, options): notification for UI - 'onDisconnected': function () { }, // onDisconnected(rfb, reason): disconnection finished - 'onPasswordRequired': function () { }, // onPasswordRequired(rfb, msg): VNC password is required - 'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received - 'onBell': function () { }, // onBell(rfb): RFB Bell message received - 'onFBUReceive': function () { }, // onFBUReceive(rfb, rect): RFB FBU rect received but not yet processed - 'onFBUComplete': function () { }, // onFBUComplete(rfb): RFB FBU received and processed - 'onFBResize': function () { }, // onFBResize(rfb, width, height): frame buffer resized - 'onDesktopName': function () { }, // onDesktopName(rfb, name): desktop name received - 'onXvpInit': function () { } // onXvpInit(version): XVP extensions active for this connection - }); - + // Bound event handlers + this._eventHandlers = { + focusCanvas: this._focusCanvas.bind(this), + }; + // main setup Log.Debug(">> RFB.constructor"); @@@ -174,19 -162,20 +167,20 @@@ // NB: nothing that needs explicit teardown should be done // before this point, since this can throw an exception try { - this._display = new Display({target: this._target, - onFlush: this._onFlush.bind(this)}); + this._display = new Display(this._target); } catch (exc) { Log.Error("Display exception: " + exc); throw exc; } + this._display.onflush = this._onFlush.bind(this); + this._display.clear(); - this._keyboard = new Keyboard({target: this._target, - onKeyEvent: this._handleKeyEvent.bind(this)}); + this._keyboard = new Keyboard(this._target); + this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); - this._mouse = new Mouse({target: this._target, - onMouseButton: this._handleMouseButton.bind(this), - onMouseMove: this._handleMouseMove.bind(this)}); + this._mouse = new Mouse(this._target); + 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.bind(this)); @@@ -236,33 -225,50 +230,51 @@@ Log.Warn("WebSocket on-error event"); }); - this._init_vars(); - this._cleanup(); - - var rmode = this._display.get_render_mode(); - Log.Info("Using native WebSockets, render mode: " + rmode); + // 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"); }; RFB.prototype = { - // Public methods - connect: function (host, port, password, path) { - this._rfb_host = host; - this._rfb_port = port; - this._rfb_password = (password !== undefined) ? password : ""; - this._rfb_path = (path !== undefined) ? path : ""; + // ===== PROPERTIES ===== - if (!this._rfb_host) { - return this._fail( - _("Must set host")); - } + dragViewport: false, ++ focusOnClick: true, - this._rfb_init_state = ''; - this._updateConnectionState('connecting'); - return true; + _viewOnly: false, + 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 viewportScale() { return this._display.scale; }, + set viewportScale(scale) { this._display.scale = scale; }, + + get clipViewport() { return this._display.clipViewport; }, + set clipViewport(viewport) { this._display.clipViewport = viewport; }, + + get isClipped() { return this._display.isClipped; }, + + // ===== PUBLIC METHODS ===== + disconnect: function () { this._updateConnectionState('disconnecting'); this._sock.off('error'); @@@ -270,13 -276,13 +282,13 @@@ this._sock.off('open'); }, - sendPassword: function (passwd) { - this._rfb_password = passwd; + sendCredentials: function (creds) { + this._rfb_credentials = creds; setTimeout(this._init_msg.bind(this), 0); }, sendCtrlAltDel: function () { - if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } + if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } Log.Info("Sending Ctrl-Alt-Del"); this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); @@@ -285,38 -291,29 +297,29 @@@ this.sendKey(KeyTable.XK_Delete, "Delete", false); this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false); this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false); - - return true; - }, - - xvpOp: function (ver, op) { - if (this._rfb_xvp_ver < ver) { return false; } - Log.Info("Sending XVP operation " + op + " (version " + ver + ")"); - this._sock.send_string("\xFA\x00" + String.fromCharCode(ver) + String.fromCharCode(op)); - return true; }, - xvpShutdown: function () { - return this.xvpOp(1, 2); + machineShutdown: function () { + this._xvpOp(1, 2); }, - xvpReboot: function () { - return this.xvpOp(1, 3); + machineReboot: function () { + this._xvpOp(1, 3); }, - xvpReset: function () { - return this.xvpOp(1, 4); + machineReset: function () { + this._xvpOp(1, 4); }, // Send a key press. If 'down' is not specified then send a down key // followed by an up key. sendKey: function (keysym, code, down) { - if (this._rfb_connection_state !== 'connected' || this._view_only) { return false; } + if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } if (down === undefined) { this.sendKey(keysym, code, true); this.sendKey(keysym, code, false); - return true; + return; } var scancode = XtScancode[code]; @@@ -330,63 -327,55 +333,55 @@@ RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode); } else { if (!keysym) { - return false; + return; } Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym); RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); } - - return true; }, clipboardPasteFrom: function (text) { - if (this._rfb_connection_state !== 'connected' || this._view_only) { return; } + if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } RFB.messages.clientCutText(this._sock, text); }, + autoscale: function (width, height) { + if (this._rfb_connection_state !== 'connected') { return; } + this._display.autoscale(width, height); + }, + + viewportChangeSize: function(width, height) { + if (this._rfb_connection_state !== 'connected') { return; } + this._display.viewportChangeSize(width, height); + }, + // Requests a change of remote desktop size. This message is an extension // and may only be sent if we have received an ExtendedDesktopSize message requestDesktopSize: function (width, height) { if (this._rfb_connection_state !== 'connected' || - this._view_only) { - return false; + this._viewOnly) { + return; } - if (this._supportsSetDesktopSize) { - RFB.messages.setDesktopSize(this._sock, width, height, - this._screen_id, this._screen_flags); - this._sock.flush(); - return true; - } else { - return false; + if (!this._supportsSetDesktopSize) { + return; } + + RFB.messages.setDesktopSize(this._sock, width, height, + this._screen_id, this._screen_flags); }, - // Private methods + // ===== PRIVATE METHODS ===== _connect: function () { Log.Debug(">> RFB.connect"); - this._init_vars(); - var uri; - if (typeof UsingSocketIO !== 'undefined') { - uri = 'http'; - } else { - uri = this._encrypt ? 'wss' : 'ws'; - } - - uri += '://' + this._rfb_host; - if(this._rfb_port) { - uri += ':' + this._rfb_port; - } - uri += '/' + this._rfb_path; - - Log.Info("connecting to " + uri); + Log.Info("connecting to " + this._url); try { // WebSocket.onopen transitions to the RFB init states - this._sock.open(uri, this._wsProtocols); + this._sock.open(this._url, ['binary']); } catch (e) { if (e.name === 'SyntaxError') { this._fail("Invalid host or port value given", e); @@@ -396,45 -385,22 +391,22 @@@ } // Always grab focus on some kind of click event - this._target.addEventListener("mousedown", this._focusCanvas); - this._target.addEventListener("touchstart", this._focusCanvas); + this._target.addEventListener("mousedown", this._eventHandlers.focusCanvas); + this._target.addEventListener("touchstart", this._eventHandlers.focusCanvas); Log.Debug("<< RFB.connect"); }, _disconnect: function () { Log.Debug(">> RFB.disconnect"); - this._target.removeEventListener("mousedown", this._focusCanvas); - this._target.removeEventListener("touchstart", this._focusCanvas); + this._target.removeEventListener("mousedown", this._eventHandlers.focusCanvas); + this._target.removeEventListener("touchstart", this._eventHandlers.focusCanvas); this._cleanup(); this._sock.close(); this._print_stats(); Log.Debug("<< RFB.disconnect"); }, - _init_vars: function () { - // reset state - this._FBU.rects = 0; - this._FBU.subrects = 0; // RRE and HEXTILE - this._FBU.lines = 0; // RAW - this._FBU.tiles = 0; // HEXTILE - this._FBU.zlibs = []; // TIGHT zlib encoders - this._mouse_buttonMask = 0; - this._mouse_arr = []; - this._rfb_tightvnc = false; - - // Clear the per connection encoding stats - var stats = this._encStats; - Object.keys(stats).forEach(function (key) { - stats[key][0] = 0; - }); - - var i; - for (i = 0; i < 4; i++) { - this._FBU.zlibs[i] = new Inflator(); - } - }, - _print_stats: function () { var stats = this._encStats; @@@ -454,27 -420,21 +426,27 @@@ }, _cleanup: function () { - if (!this._view_only) { this._keyboard.ungrab(); } - if (!this._view_only) { this._mouse.ungrab(); } + if (!this._viewOnly) { this._keyboard.ungrab(); } + if (!this._viewOnly) { this._mouse.ungrab(); } this._display.defaultCursor(); if (Log.get_logging() !== 'debug') { - // Show noVNC logo on load and when disconnected, unless in + // Show noVNC logo when disconnected, unless in // debug mode this._display.clear(); } }, - // Event handler for canvas so this points to the canvas element _focusCanvas: function(event) { // Respect earlier handlers' request to not do side-effects - if (!event.defaultPrevented) - this.focus(); + if (event.defaultPrevented) { + return; + } + + if (!this._focus_on_click) { + return; + } + + this._target.focus(); }, /* @@@ -540,7 -500,8 +512,8 @@@ // State change actions this._rfb_connection_state = state; - this._onUpdateState(this, state, oldstate); + var event = new CustomEvent("updatestate", { detail: { state: state } }); + this.dispatchEvent(event); var smsg = "New state '" + state + "', was '" + oldstate + "'."; Log.Debug(smsg); @@@ -556,14 -517,16 +529,16 @@@ switch (state) { case 'disconnected': - // Call onDisconnected callback after onUpdateState since + // Fire disconnected event after updatestate event since // we don't know if the UI only displays the latest message if (this._rfb_disconnect_reason !== "") { - this._onDisconnected(this, this._rfb_disconnect_reason); + event = new CustomEvent("disconnect", + { detail: { reason: this._rfb_disconnect_reason } }); } else { // No reason means clean disconnect - this._onDisconnected(this); + event = new CustomEvent("disconnect", { detail: {} }); } + this.dispatchEvent(event); break; case 'connecting': @@@ -576,7 -539,7 +551,7 @@@ this._disconnTimer = setTimeout(function () { this._rfb_disconnect_reason = _("Disconnect timeout"); this._updateConnectionState('disconnected'); - }.bind(this), this._disconnectTimeout * 1000); + }.bind(this), DISCONNECT_TIMEOUT * 1000); break; } }, @@@ -618,11 -581,10 +593,10 @@@ * Send a notification to the UI. Valid levels are: * 'normal'|'warn'|'error' * - * NOTE: Options could be added in the future. * NOTE: If this function is called multiple times, remember that the * interface could be only showing the latest notification. */ - _notification: function(msg, level, options) { + _notification: function(msg, level) { switch (level) { case 'normal': case 'warn': @@@ -634,11 -596,16 +608,16 @@@ return; } - if (options) { - this._onNotification(this, msg, level, options); - } else { - this._onNotification(this, msg, level); - } + var event = new CustomEvent("notification", + { detail: { message: msg, level: level } }); + this.dispatchEvent(event); + }, + + _setCapability: function (cap, val) { + this._capabilities[cap] = val; + var event = new CustomEvent("capabilities", + { detail: { capabilities: this._capabilities } }); + this.dispatchEvent(event); }, _handle_message: function () { @@@ -681,7 -648,7 +660,7 @@@ this._mouse_buttonMask &= ~bmask; } - if (this._viewportDrag) { + if (this.dragViewport) { if (down && !this._viewportDragging) { this._viewportDragging = true; this._viewportDragPos = {'x': x, 'y': y}; @@@ -693,14 -660,14 +672,14 @@@ // If the viewport didn't actually move, then treat as a mouse click event // Send the button down event here, as the button up event is sent at the end of this function - if (!this._viewportHasMoved && !this._view_only) { + if (!this._viewportHasMoved && !this._viewOnly) { RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), bmask); } this._viewportHasMoved = false; } } - if (this._view_only) { return; } // View only, skip mouse events + 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); @@@ -727,7 -694,7 +706,7 @@@ return; } - if (this._view_only) { return; } // View only, skip mouse events + 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); @@@ -768,7 -735,7 +747,7 @@@ } if (is_repeater) { - var repeaterID = this._repeaterID; + var repeaterID = "ID:" + this._repeaterID; while (repeaterID.length < 250) { repeaterID += "\0"; } @@@ -845,21 -812,20 +824,20 @@@ // authentication _negotiate_xvp_auth: function () { - var xvp_sep = this._xvp_password_sep; - var xvp_auth = this._rfb_password.split(xvp_sep); - if (xvp_auth.length < 3) { - var msg = 'XVP credentials required (user' + xvp_sep + - 'target' + xvp_sep + 'password) -- got only ' + this._rfb_password; - this._onPasswordRequired(this, msg); + if (!this._rfb_credentials.username || + !this._rfb_credentials.password || + !this._rfb_credentials.target) { + var event = new CustomEvent("credentialsrequired", + { detail: { types: ["username", "password", "target"] } }); + this.dispatchEvent(event); return false; } - var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) + - String.fromCharCode(xvp_auth[1].length) + - xvp_auth[0] + - xvp_auth[1]; + var 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_password = xvp_auth.slice(2).join(xvp_sep); this._rfb_auth_scheme = 2; return this._negotiate_authentication(); }, @@@ -867,14 -833,16 +845,16 @@@ _negotiate_std_vnc_auth: function () { if (this._sock.rQwait("auth challenge", 16)) { return false; } - if (this._rfb_password.length === 0) { - this._onPasswordRequired(this); + if (!this._rfb_credentials.password) { + var event = new CustomEvent("credentialsrequired", + { detail: { types: ["password"] } }); + this.dispatchEvent(event); return false; } // TODO(directxman12): make genDES not require an Array var challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); - var response = RFB.genDES(this._rfb_password, challenge); + var response = RFB.genDES(this._rfb_credentials.password, challenge); this._sock.send(response); this._rfb_init_state = "SecurityResult"; return true; @@@ -1105,12 -1073,14 +1085,14 @@@ } // we're past the point where we could backtrack, so it's safe to call this - this._onDesktopName(this, this._fb_name); + var event = new CustomEvent("desktopname", + { detail: { name: this._fb_name } }); + this.dispatchEvent(event); this._resize(width, height); - if (!this._view_only) { this._keyboard.grab(); } - if (!this._view_only) { this._mouse.grab(); } + if (!this._viewOnly) { this._keyboard.grab(); } + if (!this._viewOnly) { this._mouse.grab(); } this._fb_depth = 24; @@@ -1160,7 -1130,8 +1142,8 @@@ encs.push(encodings.pseudoEncodingFence); encs.push(encodings.pseudoEncodingContinuousUpdates); - if (this._local_cursor && this._fb_depth == 24) { + if (browserSupportsCursorURIs() && + !isTouchDevice && this._fb_depth == 24) { encs.push(encodings.pseudoEncodingCursor); } @@@ -1219,9 -1190,11 +1202,11 @@@ var text = this._sock.rQshiftStr(length); - if (this._view_only) { return true; } + if (this._viewOnly) { return true; } - this._onClipboard(this, text); + var event = new CustomEvent("clipboard", + { detail: { text: text } }); + this.dispatchEvent(event); return true; }, @@@ -1283,7 -1256,7 +1268,7 @@@ case 1: // XVP_INIT this._rfb_xvp_ver = xvp_ver; Log.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")"); - this._onXvpInit(this._rfb_xvp_ver); + this._setCapability("power", true); break; default: this._fail("Unexpected server message", @@@ -1317,7 -1290,8 +1302,8 @@@ case 2: // Bell Log.Debug("Bell"); - this._onBell(this); + var event = new CustomEvent("bell", { detail: {} }); + this.dispatchEvent(event); return true; case 3: // ServerCutText @@@ -1398,12 -1372,6 +1384,6 @@@ this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + (hdr[10] << 8) + hdr[11], 10); - this._onFBUReceive(this, - {'x': this._FBU.x, 'y': this._FBU.y, - 'width': this._FBU.width, 'height': this._FBU.height, - 'encoding': this._FBU.encoding, - 'encodingName': encodingName(this._FBU.encoding)}); - if (!this._encHandlers[this._FBU.encoding]) { this._fail("Unexpected server message", "Unsupported encoding " + @@@ -1458,8 -1426,6 +1438,6 @@@ this._display.flip(); - this._onFBUComplete(this); - return true; // We finished this FBU }, @@@ -1477,77 -1443,24 +1455,24 @@@ this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); this._display.resize(this._fb_width, this._fb_height); - this._onFBResize(this, this._fb_width, this._fb_height); + + var event = new CustomEvent("fbresize", + { detail: { width: this._fb_width, + height: this._fb_height } }); + this.dispatchEvent(event); this._timing.fbu_rt_start = (new Date()).getTime(); this._updateContinuousUpdates(); - } - }; - - make_properties(RFB, [ - ['target', 'wo', 'dom'], // VNC display rendering Canvas object - ['encrypt', 'rw', 'bool'], // Use TLS/SSL/wss encryption - ['local_cursor', 'rw', 'bool'], // Request locally rendered cursor - ['shared', 'rw', 'bool'], // Request shared mode - ['view_only', 'rw', 'bool'], // Disable client mouse/keyboard - ['focus_on_click', 'rw', 'bool'], // Grab focus on canvas on mouse click - ['xvp_password_sep', 'rw', 'str'], // Separator for XVP password fields - ['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection - ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection - ['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to - ['viewportDrag', 'rw', 'bool'], // Move the viewport on mouse drags - - // Callback functions - ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate): connection state change - ['onNotification', 'rw', 'func'], // onNotification(rfb, msg, level, options): notification for the UI - ['onDisconnected', 'rw', 'func'], // onDisconnected(rfb, reason): disconnection finished - ['onPasswordRequired', 'rw', 'func'], // onPasswordRequired(rfb, msg): VNC password is required - ['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received - ['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received - ['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed - ['onFBUComplete', 'rw', 'func'], // onFBUComplete(rfb, fbu): RFB FBU received and processed - ['onFBResize', 'rw', 'func'], // onFBResize(rfb, width, height): frame buffer resized - ['onDesktopName', 'rw', 'func'], // onDesktopName(rfb, name): desktop name received - ['onXvpInit', 'rw', 'func'] // onXvpInit(version): XVP extensions active for this connection - ]); - - RFB.prototype.set_local_cursor = function (cursor) { - if (!cursor || (cursor in {'0': 1, 'no': 1, 'false': 1})) { - this._local_cursor = false; - this._display.disableLocalCursor(); //Only show server-side cursor - } else { - if (this._display.get_cursor_uri()) { - this._local_cursor = true; - } else { - Log.Warn("Browser does not support local cursor"); - this._display.disableLocalCursor(); - } - } - - // Need to send an updated list of encodings if we are connected - if (this._rfb_connection_state === "connected") { - this._sendEncodings(); - } - }; - - RFB.prototype.set_view_only = function (view_only) { - this._view_only = view_only; + }, - if (this._rfb_connection_state === "connecting" || - this._rfb_connection_state === "connected") { - if (view_only) { - this._keyboard.ungrab(); - this._mouse.ungrab(); - } else { - this._keyboard.grab(); - this._mouse.grab(); - } - } + _xvpOp: function (ver, op) { + if (this._rfb_xvp_ver < ver) { return; } + Log.Info("Sending XVP operation " + op + " (version " + ver + ")"); + RFB.messages.xvpOp(this._sock, ver, op); + }, }; - RFB.prototype.get_display = function () { return this._display; }; - RFB.prototype.get_keyboard = function () { return this._keyboard; }; - RFB.prototype.get_mouse = function () { return this._mouse; }; + Object.assign(RFB.prototype, EventTargetMixin); // Class Methods RFB.messages = { @@@ -1830,7 -1743,21 +1755,21 @@@ sock._sQlen += 10; sock.flush(); - } + }, + + xvpOp: function (sock, ver, op) { + var buff = sock._sQ; + var 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.genDES = function (password, challenge) { @@@ -2361,6 -2288,8 +2300,8 @@@ RFB.encodingHandlers = if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } this._supportsSetDesktopSize = true; + this._setCapability("resize", true); + var number_of_screens = this._sock.rQpeek8(); this._FBU.bytes = 4 + (number_of_screens * 16); diff --combined docs/API.md index 2a9e93e,983d735..8f565e9 --- a/docs/API.md +++ b/docs/API.md @@@ -1,255 -1,419 +1,424 @@@ - # 1. Modules / API + # noVNC API - The noVNC client is a composed of several modular components that handle - rendering, input, networking, etc. Each of the modules is designed to - be cross-browser and be useful as a standalone library in other - projects (see LICENSE.txt). + The interface of the noVNC client consists of a single RFB object that + is instantiated once per connection. + ## RFB - ## 1.1 Module List + The `RFB` object represents a single connection to a VNC server. It + communicates using a WebSocket that must provide a standard RFB + protocol stream. - * __Mouse__ (core/input/mouse.js): Mouse input event handler with - limited touch support. + ### Constructor - * __Keyboard__ (core/input/keyboard.js): Keyboard input event handler with - non-US keyboard support. Translates keyDown and keyUp events to X11 - keysym values. + [`RFB()`](#rfb-1) + - Creates and returns a new `RFB` object. - * __Display__ (core/display.js): Efficient 2D rendering abstraction - layered on the HTML5 canvas element. + ### Properties - * __Websock__ (core/websock.js): Websock client from websockify - with transparent binary data support. - [Websock API](https://github.com/novnc/websockify/wiki/websock.js) wiki page. + `viewOnly` + - Is a `boolean` indicating if any events (e.g. key presses or mouse + movement) should be prevented from being sent to the server. + Disabled by default. - * __RFB__ (core/rfb.js): Main class that implements the RFB - protocol and stitches the other classes together. ++`focusOnClick` ++ - Is a `boolean` indicating if keyboard focus should automatically be ++ moved to the canvas when a `mousedown` or `touchstart` event is ++ received. + + `touchButton` + - Is a `long` controlling the button mask that should be simulated + when a touch event is recieved. Uses the same values as + [`MouseEvent.button`](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button). + Is set to `1` by default. - ## 1.2 Configuration Attributes + `viewportScale` + - Is a `double` indicating how the framebuffer contents should be + scaled before being rendered on to the canvas. See also + [`RFB.autoscale()`](#rfbautoscale). Is set to `1.0` by default. - The Mouse, Keyboard, Display and RFB classes have a similar API for - configuration options. Each configuration option has a default value, - which can be overridden by a a configuration object passed to the - constructor. Configuration options can then be read and modified after - initialization with "get_*" and "set_*" methods respectively. For - example, the following initializes an RFB object with the 'encrypt' - configuration option enabled, then confirms it was set, then disables - it. + `clipViewport` + - Is a `boolean` indicating if the canvas should be clipped to its + container. When disabled the container must be able to handle the + resulting overflow. Disabled by default. - var rfb = new RFB({'encrypt': true}); - if (rfb.get_encrypt()) { - alert("Encryption is set"); - } - rfb.set_encrypt(false); + `dragViewport` + - Is a `boolean` indicating if mouse events should control the + relative position of a clipped canvas. Only relevant if + `clipViewport` is enabled. Disabled by default. - Some attributes are read-only and cannot be changed. For example, the - Display 'render_mode' option will throw an exception if an attempt is - made to set it. The attribute mode is one of the following: + `isClipped` *Read only* + - Is a `boolean` indicating if the framebuffer is larger than the + current canvas, i.e. it is being clipped. - RO - read only - RW - read write - WO - write once + `capabilities` *Read only* + - Is an `Object` indicating which optional extensions are available + on the server. Some methods may only be called if the corresponding + capability is set. The following capabilities are defined: + | name | type | description + | -------- | --------- | ----------- + | `power` | `boolean` | Machine power control is available + | `resize` | `boolean` | The framebuffer can be resized - ## 1.3 Methods + ### Events - In addition to the getter and setter methods to modify configuration - attributes, each of the modules has other methods that are available - in the object instance. For example, the Display module has method - named 'blitImage' which takes an array of pixel data and draws it to - the 2D canvas. + [`updatestate`](#updatestate) + - The `updatestate` event is fired when the connection state of the + `RFB` object changes. - ## 1.4 Callbacks + [`notification`](#notification) + - The `notification` event is fired when the `RFB` usage has a + message to display to the user. - Each of the modules has certain events that can be hooked with - callback functions. For the Mouse, Keyboard, Display and RFB classes - the callback functions are assigned to configuration attributes. The - WebSock module has a method named 'on' that takes two parameters: the - callback event name, and the callback function. + [`disconnect`](#disconnected) + - The `disconnect` event is fired when the `RFB` object disconnects. - ## 2. Modules + [`credentialsrequired`](#credentialsrequired) + - The `credentialsrequired` event is fired when more credentials must + be given to continue. - ## 2.1 Mouse Module + [`clipboard`](#clipboard) + - The `clipboard` event is fired when clipboard data is received from + the server. - ### 2.1.1 Configuration Attributes + [`bell`](#bell) + - The `bell` event is fired when a audible bell request is received + from the server. - | name | type | mode | default | description - | ----------- | ---- | ---- | -------- | ------------ - | target | DOM | WO | document | DOM element that captures mouse input - | touchButton | int | RW | 1 | Button mask (1, 2, 4) for which click to send on touch devices. 0 means ignore clicks. + [`fbresize`](#fbresize) + - The `fbresize` event is fired when the framebuffer size is changed. - ### 2.1.2 Methods + [`desktopname`](#desktopname) + - The `desktopname` event is fired when the remote desktop name + changes. - | name | parameters | description - | ------ | ---------- | ------------ - | grab | () | Begin capturing mouse events - | ungrab | () | Stop capturing mouse events + [`capabilities`](#capabilities) + - The `capabilities` event is fired when `RFB.capabilities` is + updated. - ### 2.1.2 Callbacks + ### Methods - | name | parameters | description - | ------------- | ------------------- | ------------ - | onMouseButton | (x, y, down, bmask) | Handler for mouse button click/release - | onMouseMove | (x, y) | Handler for mouse movement + [`RFB.disconnect()`](#rfbdisconnect) + - Disconnect from the server. + [`RFB.sendCredentials()`](#rfbsendcredentials) + - Send credentials to server. Should be called after the + [`credentialsrequired`](#credentialsrequired) event has fired. - ## 2.2 Keyboard Module + [`RFB.sendKey()`](#rfbsendKey) + - Send a key event. - ### 2.2.1 Configuration Attributes - - | name | type | mode | default | description - | ------- | ---- | ---- | -------- | ------------ - | target | DOM | WO | document | DOM element that captures keyboard input - - ### 2.2.2 Methods - - | name | parameters | description - | ------ | ---------- | ------------ - | grab | () | Begin capturing keyboard events - | ungrab | () | Stop capturing keyboard events - - ### 2.2.3 Callbacks - - | name | parameters | description - | ---------- | -------------------- | ------------ - | onKeyPress | (keysym, code, down) | Handler for key press/release - - - ## 2.3 Display Module - - ### 2.3.1 Configuration Attributes - - | name | type | mode | default | description - | ----------- | ----- | ---- | ------- | ------------ - | target | DOM | WO | | Canvas element for rendering - | context | raw | RO | | Canvas 2D context for rendering - | logo | raw | RW | | Logo to display when cleared: {"width": width, "height": height, "type": mime-type, "data": data} - | scale | float | RW | 1.0 | Display area scale factor 0.0 - 1.0 - | viewport | bool | RW | false | Use viewport clipping - | width | int | RO | | Display area width - | height | int | RO | | Display area height - | render_mode | str | RO | '' | Canvas rendering mode - | prefer_js | str | RW | | Prefer JavaScript over canvas methods - | cursor_uri | raw | RW | | Can we render cursor using data URI - - ### 2.3.2 Methods - - | name | parameters | description - | ------------------ | ------------------------------------------------------- | ------------ - | viewportChangePos | (deltaX, deltaY) | Move the viewport relative to the current location - | viewportChangeSize | (width, height) | Change size of the viewport - | absX | (x) | Return X relative to the remote display - | absY | (y) | Return Y relative to the remote display - | resize | (width, height) | Set width and height - | flip | (from_queue) | Update the visible canvas with the contents of the rendering canvas - | clear | () | Clear the display (show logo if set) - | pending | () | Check if there are waiting items in the render queue - | flush | () | Resume processing the render queue unless it's empty - | fillRect | (x, y, width, height, color, from_queue) | Draw a filled in rectangle - | copyImage | (old_x, old_y, new_x, new_y, width, height, from_queue) | Copy a rectangular area - | imageRect | (x, y, mime, arr) | Draw a rectangle with an image - | startTile | (x, y, width, height, color) | Begin updating a tile - | subTile | (tile, x, y, w, h, color) | Update a sub-rectangle within the given tile - | finishTile | () | Draw the current tile to the display - | blitImage | (x, y, width, height, arr, offset, from_queue) | Blit pixels (of R,G,B,A) to the display - | blitRgbImage | (x, y, width, height, arr, offset, from_queue) | Blit RGB encoded image to display - | blitRgbxImage | (x, y, width, height, arr, offset, from_queue) | Blit RGBX encoded image to display - | drawImage | (img, x, y) | Draw image and track damage - | changeCursor | (pixels, mask, hotx, hoty, w, h) | Change cursor appearance - | defaultCursor | () | Restore default cursor appearance - | disableLocalCursor | () | Disable local (client-side) cursor - | clippingDisplay | () | Check if the remote display is larger than the client display - | autoscale | (containerWidth, containerHeight, downscaleOnly) | Scale the display - - ### 2.3.3 Callbacks - - | name | parameters | description - | ------- | ---------- | ------------ - | onFlush | () | A display flush has been requested and we are now ready to resume FBU processing - - - ## 2.4 RFB Module - - ### 2.4.1 Configuration Attributes - - | name | type | mode | default | description - | ----------------- | ---- | ---- | ---------- | ------------ - | target | DOM | WO | null | Canvas element for rendering (passed to Display, Mouse and Keyboard) - | encrypt | bool | RW | false | Use TLS/SSL encryption - | local_cursor | bool | RW | false | Request locally rendered cursor - | shared | bool | RW | true | Request shared VNC mode - | view_only | bool | RW | false | Disable client mouse/keyboard - | focus_on_click | bool | RW | true | Grab focus on canvas on mouse click - | xvp_password_sep | str | RW | '@' | Separator for XVP password fields - | disconnectTimeout | int | RW | 3 | Time (in seconds) to wait for disconnection - | wsProtocols | arr | RW | ['binary'] | Protocols to use in the WebSocket connection - | repeaterID | str | RW | '' | UltraVNC RepeaterID to connect to - | viewportDrag | bool | RW | false | Move the viewport on mouse drags - - ### 2.4.2 Methods - - | name | parameters | description - | ------------------ | ---------------------------- | ------------ - | connect | (host, port, password, path) | Connect to the given host:port/path. Optional password and path. - | disconnect | () | Disconnect - | sendPassword | (passwd) | Send password after onPasswordRequired callback - | sendCtrlAltDel | () | Send Ctrl-Alt-Del key sequence - | xvpOp | (ver, op) | Send a XVP operation (2=shutdown, 3=reboot, 4=reset) - | xvpShutdown | () | Send XVP shutdown. - | xvpReboot | () | Send XVP reboot. - | xvpReset | () | Send XVP reset. - | sendKey | (keysym, code, down) | Send a key press event. If down not specified, send a down and up event. - | clipboardPasteFrom | (text) | Send a clipboard paste event - | requestDesktopSize | (width, height) | Send a request to change the remote desktop size. - - ### 2.4.3 Callbacks - - | name | parameters | description - | ------------------ | -------------------------- | ------------ - | onUpdateState | (rfb, state, oldstate) | Connection state change (see details below) - | onNotification | (rfb, msg, level, options) | Notification for the UI (optional options) - | onDisconnected | (rfb, reason) | Disconnection finished with an optional reason. No reason specified means normal disconnect. - | onPasswordRequired | (rfb, msg) | VNC password is required (use sendPassword), optionally comes with a message. - | onClipboard | (rfb, text) | RFB clipboard contents received - | onBell | (rfb) | RFB Bell message received - | onFBUReceive | (rfb, fbu) | RFB FBU received but not yet processed (see details below) - | onFBUComplete | (rfb, fbu) | RFB FBU received and processed (see details below) - | onFBResize | (rfb, width, height) | Frame buffer (remote desktop) size changed - | onDesktopName | (rfb, name) | VNC desktop name recieved - | onXvpInit | (version) | XVP extensions active for this connection. - - - __RFB onUpdateState callback details__ - - The RFB module has an 'onUpdateState' callback that is invoked after - the noVNC connection state changes. Here is a list of the states that - are reported. Note that the RFB module can not transition from the - disconnected state in any way, a new instance of the object has to be - created for new connections. - - | connection state | description - | ---------------- | ------------ - | connecting | starting to connect - | connected | connected normally - | disconnecting | starting to disconnect - | disconnected | disconnected - permanent end-state for this RFB object - - __RFB onFBUReceive and on FBUComplete callback details__ - - The onFBUReceive callback is invoked when a frame buffer update - message has been received from the server but before the RFB class has - done any additional handling. The onFBUComplete callback is invoked - with the same information but after the RFB class has handled the - message. - - The 'fbu' parameter is an object with the following structure: - - { - x: FBU_x_position, - y: FBU_y_position, - width: FBU_width, - height: FBU_height, - encoding: FBU_encoding_number, - encodingName: FBU_encoding_string - } + [`RFB.sendCtrlAltDel()`](#rfbsendctrlaltdel) + - Send Ctrl-Alt-Del key sequence. + + [`RFB.machineShutdown()`](#rfbmachineshutdown) + - Request a shutdown of the remote machine. + + [`RFB.machineReboot()`](#rfbmachinereboot) + - Request a reboot of the remote machine. + + [`RFB.machineReset()`](#rfbmachinereset) + - Request a reset of the remote machine. + + [`RFB.clipboardPasteFrom()`](#rfbclipboardPasteFrom) + - Send clipboard contents to server. + + [`RFB.autoscale()`](#rfbautoscale) + - Set `RFB.viewportScale` so that the framebuffer fits a specified + container. + + [`RFB.requestDesktopSize()`](#rfbrequestDesktopSize) + - Send a request to change the remote desktop size. + + [`RFB.viewportChangeSize()`](#rfbviewportChangeSize) + - Change size of the viewport. + + ### Details + + #### RFB() + + The `RFB()` constructor returns a new `RFB` object and initiates a new + connection to a specified VNC server. + + ##### Syntax + + var rfb = new RFB( target, url [, options] ); + + ###### Parameters + + **`target`** + - A [`HTMLCanvasElement`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement) + that specifies where graphics should be rendered and input events + should be monitored. + + **`url`** + - A `DOMString` specifying the VNC server to connect to. This must be + a valid WebSocket URL. + + **`options`** *Optional* + - An `Object` specifying extra details about how the connection + should be made. + + Possible options: + + `shared` + - A `boolean` indicating if the remote server should be shared or + if any other connected clients should be disconnected. Enabled + by default. + + `credentials` + - An `Object` specifying the credentials to provide to the server + when authenticating. The following credentials are possible: + + | name | type | description + | ------------ | ----------- | ----------- + | `"username"` | `DOMString` | The user that authenticates + | `"password"` | `DOMString` | Password for the user + | `"target"` | `DOMString` | Target machine or session + + `repeaterID` + - A `DOMString` specifying the ID to provide to any VNC repeater + encountered. + + #### updatestate + + The `updatestate` event is fired after the noVNC connection state + changes. The `detail` property is an `Object` containg the property + `state` with the new connection state. + + Here is a list of the states that are reported: + + | connection state | description + | ----------------- | ------------ + | `"connecting"` | starting to connect + | `"connected"` | connected normally + | `"disconnecting"` | starting to disconnect + | `"disconnected"` | disconnected + + Note that a `RFB` objects can not transition from the disconnected + state in any way, a new instance of the object has to be created for + new connections. + + #### notification + + The `notification` event is fired when the `RFB` object wants a message + displayed to the user. The `detail` property is an `Object` containing + the following properties: + + | Property | Type | Description + | --------- | ----------- | ----------- + | `message` | `DOMString` | The message to display + | `level` | `DOMString` | The severity of the message + + The following levels are currently defined: + + - `"normal"` + - `"warn"` + - `"error"` + + #### disconnect + + The `disconnect` event is fired when the connection has been + terminated. The `detail` property is an `Object` the optionally + contains the property `reason`. `reason` is a `DOMString` specifying + the reason in the event of an unexpected termination. `reason` will be + omitted for a clean termination. + + #### credentialsrequired + + The `credentialsrequired` event is fired when the server requests more + credentials than were specified to [`RFB()`](#rfb-1). The `detail` + property is an `Object` containing the property `types` which is an + `Array` of `DOMString` listing the credentials that are required. + + #### clipboard + + The `clipboard` event is fired when the server has sent clipboard data. + The `detail` property is an `Object` containing the property `text` + which is a `DOMString` with the clipboard data. + + #### bell + + The `bell` event is fired when the server has requested an audible + bell. + + #### fbresize + + The `fbresize` event is fired when the framebuffer has changed + dimensions. The `detail` property is an `Object` with the properties + `width` and `height` specifying the new dimensions. + + #### desktopname + + The `desktopname` event is fired when the name of the remote desktop + changes. The `detail` property is an `Object` with the property `name` + which is a `DOMString` specifying the new name. + + #### capabilities + + The `capabilities` event is fired whenever an entry is added or removed + from `RFB.capabilities`. The `detail` property is an `Object` with the + property `capabilities` containing the new value of `RFB.capabilities`. + + #### RFB.disconnect() + + The `RFB.disconnect()` method is used to disconnect from the currently + connected server. + + ##### Syntax + + RFB.disconnect( ); + + #### RFB.sendCredentials() + + The `RFB.sendCredentials()` method is used to provide the missing + credentials after a `credentialsrequired` event has been fired. + + ##### Syntax + + RFB.sendCredentials( credentials ); + + ###### Parameters + + **`credentials`** + - An `Object` specifying the credentials to provide to the server + when authenticating. See [`RFB()`](#rfb-1) for details. + + #### RFB.sendKey() + + The `RFB.sendKey()` method is used to send a key event to the server. + + ##### Syntax + + RFB.sendKey( keysym, code [, down] ); + + ###### Parameters + + **`keysym`** + - A `long` specifying the RFB keysym to send. Can be `0` if a valid + **`code`** is specified. + + **`code`** + - A `DOMString` specifying the physical key to send. Valid values are + those that can be specified to + [`KeyboardEvent.code`](https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code). + If the physical key cannot be determined then `null` shall be + specified. + + **`down`** *Optional* + - A `boolean` specifying if a press or a release event should be + sent. If omitted then both a press and release event are sent. + + #### RFB.sendCtrlAltDel() + + The `RFB.sendCtrlAltDel()` method is used to send the key sequence + *left Control*, *left Alt*, *Delete*. This is a convenience wrapper + around [`RFB.sendKey()`](#rfbsendkey). + + ##### Syntax + + RFB.sendCtrlAltDel( ); + + #### RFB.machineShutdown() + + The `RFB.machineShutdown()` method is used to request to shut down the + remote machine. The capability `power` must be set for this method to + have any effect. + + ##### Syntax + + RFB.machineShutdown( ); + + #### RFB.machineReboot() + + The `RFB.machineReboot()` method is used to request a clean reboot of + the remote machine. The capability `power` must be set for this method + to have any effect. + + ##### Syntax + + RFB.machineReboot( ); + + #### RFB.machineReset() + + The `RFB.machineReset()` method is used to request a forced reset of + the remote machine. The capability `power` must be set for this method + to have any effect. + + ##### Syntax + + RFB.machineReset( ); + + #### RFB.clipboardPasteFrom() + + The `RFB.clipboardPasteFrom()` method is used to send clipboard data + to the remote server. + + ##### Syntax + + RFB.clipboardPasteFrom( text ); + + ###### Parameters + + **`text`** + - A `DOMString` specifying the clipboard data to send. Currently only + characters from ISO 8859-1 are supported. + + #### RFB.autoscale() + + The `RFB.autoscale()` method is used to automatically adjust + `RFB.viewportScale` to fit given dimensions. + + ##### Syntax + + RFB.autoscale( width, height ); + + ###### Parameters + + **`width`** + - A `long` specifying the maximum width of the canvas in CSS pixels. + + **`height`** + - A `long` specifying the maximum height of the canvas in CSS pixels. + + #### RFB.requestDesktopSize() + + The `RFB.requestDesktopSize()` method is used to request a change of + the framebuffer. The capability `resize` must be set for this method to + have any effect. + + Note that this is merely a request and the server may deny it. + The [`fbresize`](#fbresize) event will be fired when the framebuffer + actually changes dimensions. + + ##### Syntax + + RFB.requestDesktopSize( width, height ); + + ###### Parameters + + **`width`** + - A `long` specifying the new requested width in CSS pixels. + + **`height`** + - A `long` specifying the new requested height in CSS pixels. + + #### RFB.viewportChangeSize() + + The `RFB.viewportChangeSize()` method is used to change the size of the + canvas rather than the underlying framebuffer. + + This method has no effect if `RFB.clipViewport` is set to `false`. + + ##### Syntax + + RFB.viewportChangeSize( width, height ); + + ###### Parameters + + **`width`** + - A `long` specifying the new width in CSS pixels. + + **`height`** + - A `long` specifying the new height in CSS pixels. diff --combined vnc_lite.html index 671d78e,6575b46..d817885 --- a/vnc_lite.html +++ b/vnc_lite.html @@@ -5,7 -5,7 +5,7 @@@