]> git.proxmox.com Git - mirror_novnc.git/blobdiff - core/rfb.js
Switch to URL for connect()
[mirror_novnc.git] / core / rfb.js
index 430fa79810a8889fa4f7199abc3391e4b456326a..ec413a84f74140eaae7691bf036a121e4d6184f5 100644 (file)
@@ -15,13 +15,15 @@ import _ from './util/localization.js';
 import { decodeUTF8 } from './util/strings.js';
 import { set_defaults, make_properties } from './util/properties.js';
 import Display from "./display.js";
-import { Keyboard, Mouse } from "./input/devices.js";
+import Keyboard from "./input/keyboard.js";
+import Mouse from "./input/mouse.js";
 import Websock from "./websock.js";
 import Base64 from "./base64.js";
 import DES from "./des.js";
 import KeyTable from "./input/keysym.js";
 import XtScancode from "./input/xtscancodes.js";
 import Inflator from "./inflator.js";
+import { encodings, encodingName } from "./encodings.js";
 
 /*jslint white: false, browser: true */
 /*global window, Util, Display, Keyboard, Mouse, Websock, Websock_native, Base64, DES, KeyTable, Inflator, XtScancode */
@@ -32,68 +34,57 @@ export default function RFB(defaults) {
         defaults = {};
     }
 
-    this._rfb_host = '';
-    this._rfb_port = 5900;
-    this._rfb_password = '';
-    this._rfb_path = '';
+    // Connection details
+    this._url = '';
+    this._rfb_credentials = {};
 
+    // 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;
 
-    // In preference order
-    this._encodings = [
-        ['COPYRECT',             0x01 ],
-        ['TIGHT',                0x07 ],
-        ['TIGHT_PNG',            -260 ],
-        ['HEXTILE',              0x05 ],
-        ['RRE',                  0x02 ],
-        ['RAW',                  0x00 ],
+    this._fb_width = 0;
+    this._fb_height = 0;
 
-        // Psuedo-encoding settings
+    this._fb_name = "";
 
-        //['JPEG_quality_lo',     -32 ],
-        ['JPEG_quality_med',      -26 ],
-        //['JPEG_quality_hi',     -23 ],
-        //['compress_lo',        -255 ],
-        ['compress_hi',          -247 ],
-
-        ['DesktopSize',          -223 ],
-        ['last_rect',            -224 ],
-        ['Cursor',               -239 ],
-        ['QEMUExtendedKeyEvent', -258 ],
-        ['ExtendedDesktopSize',  -308 ],
-        ['xvp',                  -309 ],
-        ['Fence',                -312 ],
-        ['ContinuousUpdates',    -313 ]
-    ];
+    this._capabilities = { power: false, resize: false };
 
-    this._encHandlers = {};
-    this._encNames = {};
-    this._encStats = {};
+    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,
@@ -104,14 +95,11 @@ export default function RFB(defaults) {
         encoding: 0,
         subencoding: -1,
         background: null,
-        zlib: []                // TIGHT zlib streams
+        zlibs: []               // TIGHT zlib streams
     };
-
-    this._fb_Bpp = 4;
-    this._fb_depth = 3;
-    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)
@@ -131,10 +119,6 @@ export default function RFB(defaults) {
         pixels: 0
     };
 
-    this._supportsSetDesktopSize = false;
-    this._screen_id = 0;
-    this._screen_flags = 0;
-
     // Mouse state
     this._mouse_buttonMask = 0;
     this._mouse_arr = [];
@@ -142,19 +126,12 @@ export default function RFB(defaults) {
     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
-        'focusContainer': document,             // DOM element that captures keyboard input
-        'encrypt': false,                       // Use TLS/SSL/wss encryption
-        'true_color': true,                     // Request true color pixel data
         'local_cursor': false,                  // Request locally rendered cursor
         'shared': true,                         // Request shared mode
         'view_only': false,                     // Disable client mouse/keyboard
-        '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
@@ -164,31 +141,35 @@ export default function RFB(defaults) {
         '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
+        'onCredentialsRequired': function () { }, // onCredentialsRequired(rfb, types): VNC credentials are required
         'onClipboard': function () { },         // onClipboard(rfb, text): RFB clipboard contents received
         'onBell': function () { },              // onBell(rfb): RFB Bell message received
-        'onFBUReceive': function () { },        // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed
-        'onFBUComplete': function () { },       // onFBUComplete(rfb, fbu): 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
+        'onCapabilities': function () { }       // onCapabilities(rfb, caps): the supported capabilities has changed
     });
 
     // main setup
     Log.Debug(">> RFB.constructor");
 
-    // populate encHandlers with bound versions
-    Object.keys(RFB.encodingHandlers).forEach(function (encName) {
-        this._encHandlers[encName] = RFB.encodingHandlers[encName].bind(this);
-    }.bind(this));
-
-    // Create lookup tables based on encoding number
-    for (var i = 0; i < this._encodings.length; i++) {
-        this._encHandlers[this._encodings[i][1]] = this._encHandlers[this._encodings[i][0]];
-        this._encNames[this._encodings[i][1]] = this._encodings[i][0];
-        this._encStats[this._encodings[i][1]] = [0, 0];
+    // Target canvas must be able to have focus
+    if (!this._target.hasAttribute('tabindex')) {
+        this._target.tabIndex = -1;
     }
 
+    // populate encHandlers with bound versions
+    this._encHandlers[encodings.encodingRaw] = RFB.encodingHandlers.RAW.bind(this);
+    this._encHandlers[encodings.encodingCopyRect] = RFB.encodingHandlers.COPYRECT.bind(this);
+    this._encHandlers[encodings.encodingRRE] = RFB.encodingHandlers.RRE.bind(this);
+    this._encHandlers[encodings.encodingHextile] = RFB.encodingHandlers.HEXTILE.bind(this);
+    this._encHandlers[encodings.encodingTight] = RFB.encodingHandlers.TIGHT.bind(this);
+
+    this._encHandlers[encodings.pseudoEncodingDesktopSize] = RFB.encodingHandlers.DesktopSize.bind(this);
+    this._encHandlers[encodings.pseudoEncodingLastRect] = RFB.encodingHandlers.last_rect.bind(this);
+    this._encHandlers[encodings.pseudoEncodingCursor] = RFB.encodingHandlers.Cursor.bind(this);
+    this._encHandlers[encodings.pseudoEncodingQEMUExtendedKeyEvent] = RFB.encodingHandlers.QEMUExtendedKeyEvent.bind(this);
+    this._encHandlers[encodings.pseudoEncodingExtendedDesktopSize] = RFB.encodingHandlers.ExtendedDesktopSize.bind(this);
+
     // NB: nothing that needs explicit teardown should be done
     // before this point, since this can throw an exception
     try {
@@ -198,8 +179,9 @@ export default function RFB(defaults) {
         Log.Error("Display exception: " + exc);
         throw exc;
     }
+    this._display.clear();
 
-    this._keyboard = new Keyboard({target: this._focusContainer,
+    this._keyboard = new Keyboard({target: this._target,
                                    onKeyEvent: this._handleKeyEvent.bind(this)});
 
     this._mouse = new Mouse({target: this._target,
@@ -254,9 +236,6 @@ export default function RFB(defaults) {
         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);
 
@@ -265,15 +244,13 @@ export default function RFB(defaults) {
 
 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 : "";
+    connect: function (url, creds) {
+        this._url = url;
+        this._rfb_credentials = (creds !== undefined) ? creds : {};
 
-        if (!this._rfb_host || !this._rfb_port) {
-            return this._fail(
-                _("Must set host and port"));
+        if (!url) {
+            this._fail(_("Must specify URL"));
+            return;
         }
 
         this._rfb_init_state = '';
@@ -288,8 +265,8 @@ RFB.prototype = {
         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);
     },
 
@@ -307,23 +284,16 @@ RFB.prototype = {
         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
@@ -337,20 +307,19 @@ RFB.prototype = {
             return true;
         }
 
-        if (this._qemuExtKeyEventSupported) {
-            var scancode = XtScancode[code];
+        var scancode = XtScancode[code];
 
-            if (scancode === undefined) {
-                Log.Error('Unable to find a xt scancode for code: ' + code);
-                // FIXME: not in the spec, but this is what
-                // gtk-vnc does
-                scancode = 0;
-            }
+        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 false;
+            }
             Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym);
             RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0);
         }
@@ -363,6 +332,21 @@ RFB.prototype = {
         RFB.messages.clientCutText(this._sock, text);
     },
 
+    autoscale: function (width, height, downscaleOnly) {
+        if (this._rfb_connection_state !== 'connected') { return; }
+        this._display.autoscale(width, height, downscaleOnly);
+    },
+
+    viewportChangeSize: function(width, height) {
+        if (this._rfb_connection_state !== 'connected') { return; }
+        this._display.viewportChangeSize(width, height);
+    },
+
+    clippingDisplay: function () {
+        if (this._rfb_connection_state !== 'connected') { return false; }
+        return this._display.clippingDisplay();
+    },
+
     // 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) {
@@ -386,21 +370,12 @@ RFB.prototype = {
 
     _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 + ':' + this._rfb_port + '/' + 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, this._wsProtocols);
         } catch (e) {
             if (e.name === 'SyntaxError') {
                 this._fail("Invalid host or port value given", e);
@@ -409,54 +384,39 @@ RFB.prototype = {
             }
         }
 
+        // Always grab focus on some kind of click event
+        this._target.addEventListener("mousedown", this._focusCanvas);
+        this._target.addEventListener("touchstart", this._focusCanvas);
+
         Log.Debug("<< RFB.connect");
     },
 
     _disconnect: function () {
         Log.Debug(">> RFB.disconnect");
+        this._target.removeEventListener("mousedown", this._focusCanvas);
+        this._target.removeEventListener("touchstart", this._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 i;
-        for (i = 0; i < this._encodings.length; i++) {
-            this._encStats[this._encodings[i][1]][0] = 0;
-        }
-
-        for (i = 0; i < 4; i++) {
-            this._FBU.zlibs[i] = new Inflator();
-        }
-    },
-
     _print_stats: function () {
+        var stats = this._encStats;
+
         Log.Info("Encoding stats for this connection:");
-        var i, s;
-        for (i = 0; i < this._encodings.length; i++) {
-            s = this._encStats[this._encodings[i][1]];
+        Object.keys(stats).forEach(function (key) {
+            var s = stats[key];
             if (s[0] + s[1] > 0) {
-                Log.Info("    " + this._encodings[i][0] + ": " + s[0] + " rects");
+                Log.Info("    " + encodingName(key) + ": " + s[0] + " rects");
             }
-        }
+        });
 
         Log.Info("Encoding stats since page load:");
-        for (i = 0; i < this._encodings.length; i++) {
-            s = this._encStats[this._encodings[i][1]];
-            Log.Info("    " + this._encodings[i][0] + ": " + s[1] + " rects");
-        }
+        Object.keys(stats).forEach(function (key) {
+            var s = stats[key];
+            Log.Info("    " + encodingName(key) + ": " + s[1] + " rects");
+        });
     },
 
     _cleanup: function () {
@@ -464,12 +424,19 @@ RFB.prototype = {
         if (!this._view_only) { 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();
+    },
+
     /*
      * Connection states:
      *   connecting
@@ -634,6 +601,11 @@ RFB.prototype = {
         }
     },
 
+    _setCapability: function (cap, val) {
+        this._capabilities[cap] = val;
+        this._onCapabilities(this, this._capabilities);
+    },
+
     _handle_message: function () {
         if (this._sock.rQlen() === 0) {
             Log.Warn("handle_message called on an empty receive queue");
@@ -671,7 +643,7 @@ RFB.prototype = {
         if (down) {
             this._mouse_buttonMask |= bmask;
         } else {
-            this._mouse_buttonMask ^= bmask;
+            this._mouse_buttonMask &= ~bmask;
         }
 
         if (this._viewportDrag) {
@@ -838,36 +810,33 @@ RFB.prototype = {
 
     // 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) {
+            this._onCredentialsRequired(this, ["username", "password", "target"]);
             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();
     },
 
     _negotiate_std_vnc_auth: function () {
-        if (this._rfb_password.length === 0) {
-            this._onPasswordRequired(this);
+        if (this._sock.rQwait("auth challenge", 16)) { return false; }
+
+        if (!this._rfb_credentials.password) {
+            this._onCredentialsRequired(this, ["password"]);
             return false;
         }
 
-        if (this._sock.rQwait("auth challenge", 16)) { 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;
@@ -1011,7 +980,6 @@ RFB.prototype = {
                 } else {
                     return this._fail("Authentication failure");
                 }
-                return false;
             case 2:
                 return this._fail("Too many authentication attempts");
             default:
@@ -1024,9 +992,8 @@ RFB.prototype = {
         if (this._sock.rQwait("server initialization", 24)) { return false; }
 
         /* Screen size */
-        this._fb_width  = this._sock.rQshift16();
-        this._fb_height = this._sock.rQshift16();
-        this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4);
+        var width = this._sock.rQshift16();
+        var height = this._sock.rQshift16();
 
         /* PIXEL_FORMAT */
         var bpp         = this._sock.rQshift8();
@@ -1076,7 +1043,7 @@ RFB.prototype = {
 
         // NB(directxman12): these are down here so that we don't run them multiple times
         //                   if we backtrack
-        Log.Info("Screen: " + this._fb_width + "x" + this._fb_height +
+        Log.Info("Screen: " + width + "x" + height +
                   ", bpp: " + bpp + ", depth: " + depth +
                   ", big_endian: " + big_endian +
                   ", true_color: " + true_color +
@@ -1102,37 +1069,66 @@ RFB.prototype = {
         // we're past the point where we could backtrack, so it's safe to call this
         this._onDesktopName(this, this._fb_name);
 
-        if (this._true_color && this._fb_name === "Intel(r) AMT KVM") {
-            Log.Warn("Intel AMT KVM only supports 8/16 bit depths.  Disabling true color");
-            this._true_color = false;
-        }
-
-        this._display.set_true_color(this._true_color);
-        this._display.resize(this._fb_width, this._fb_height);
-        this._onFBResize(this, this._fb_width, this._fb_height);
+        this._resize(width, height);
 
         if (!this._view_only) { this._keyboard.grab(); }
         if (!this._view_only) { this._mouse.grab(); }
 
-        if (this._true_color) {
-            this._fb_Bpp = 4;
-            this._fb_depth = 3;
-        } else {
-            this._fb_Bpp = 1;
-            this._fb_depth = 1;
+        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_Bpp, this._fb_depth, this._true_color);
-        RFB.messages.clientEncodings(this._sock, this._encodings, this._local_cursor, this._true_color);
+        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._timing.fbu_rt_start = (new Date()).getTime();
         this._timing.pixels = 0;
 
+        // Cursor will be server side until the server decides to honor
+        // our request and send over the cursor image
+        this._display.disableLocalCursor();
+
         this._updateConnectionState('connected');
         return true;
     },
 
+    _sendEncodings: function () {
+        var 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.encodingHextile);
+            encs.push(encodings.encodingRRE);
+        }
+        encs.push(encodings.encodingRaw);
+
+        // Psuedo-encoding settings
+        encs.push(encodings.pseudoEncodingTightPNG);
+        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._local_cursor && this._fb_depth == 24) {
+            encs.push(encodings.pseudoEncodingCursor);
+        }
+
+        RFB.messages.clientEncodings(this._sock, encs);
+    },
+
     /* RFB protocol initialization states:
      *   ProtocolVersion
      *   Security
@@ -1171,22 +1167,8 @@ RFB.prototype = {
 
     _handle_set_colour_map_msg: function () {
         Log.Debug("SetColorMapEntries");
-        this._sock.rQskip8();  // Padding
-
-        var first_colour = this._sock.rQshift16();
-        var num_colours = this._sock.rQshift16();
-        if (this._sock.rQwait('SetColorMapEntries', num_colours * 6, 6)) { return false; }
-
-        for (var c = 0; c < num_colours; c++) {
-            var red = parseInt(this._sock.rQshift16() / 256, 10);
-            var green = parseInt(this._sock.rQshift16() / 256, 10);
-            var blue = parseInt(this._sock.rQshift16() / 256, 10);
-            this._display.set_colourMap([blue, green, red], first_colour + c);
-        }
-        Log.Debug("colourMap: " + this._display.get_colourMap());
-        Log.Info("Registered " + num_colours + " colourMap entries");
 
-        return true;
+        return this._fail("Protocol error", "Unexpected SetColorMapEntries message");
     },
 
     _handle_server_cut_text: function () {
@@ -1263,7 +1245,7 @@ RFB.prototype = {
             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",
@@ -1378,13 +1360,7 @@ RFB.prototype = {
                 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': this._encNames[this._FBU.encoding]});
-
-                if (!this._encNames[this._FBU.encoding]) {
+                if (!this._encHandlers[this._FBU.encoding]) {
                     this._fail("Unexpected server message",
                                "Unsupported encoding " +
                                this._FBU.encoding);
@@ -1400,6 +1376,9 @@ RFB.prototype = {
             this._timing.cur_fbu += (now - this._timing.last_fbu);
 
             if (ret) {
+                if (!(this._FBU.encoding in this._encStats)) {
+                    this._encStats[this._FBU.encoding] = [0, 0];
+                }
                 this._encStats[this._FBU.encoding][0]++;
                 this._encStats[this._FBU.encoding][1]++;
                 this._timing.pixels += this._FBU.width * this._FBU.height;
@@ -1435,12 +1414,6 @@ RFB.prototype = {
 
         this._display.flip();
 
-        this._onFBUComplete(this,
-                {'x': this._FBU.x, 'y': this._FBU.y,
-                 'width': this._FBU.width, 'height': this._FBU.height,
-                 'encoding': this._FBU.encoding,
-                 'encodingName': this._encNames[this._FBU.encoding]});
-
         return true;  // We finished this FBU
     },
 
@@ -1449,35 +1422,52 @@ RFB.prototype = {
 
         RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0,
                                              this._fb_width, this._fb_height);
-    }
+    },
+
+    _resize: function(width, height) {
+        this._fb_width = width;
+        this._fb_height = height;
+
+        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);
+
+        this._timing.fbu_rt_start = (new Date()).getTime();
+        this._updateContinuousUpdates();
+    },
+
+    _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);
+    },
 };
 
 make_properties(RFB, [
     ['target', 'wo', 'dom'],                // VNC display rendering Canvas object
-    ['focusContainer', 'wo', 'dom'],        // DOM element that captures keyboard input
-    ['encrypt', 'rw', 'bool'],              // Use TLS/SSL/wss encryption
-    ['true_color', 'rw', 'bool'],           // Request true color pixel data
     ['local_cursor', 'rw', 'bool'],         // Request locally rendered cursor
     ['shared', 'rw', 'bool'],               // Request shared mode
     ['view_only', 'rw', 'bool'],            // Disable client mouse/keyboard
-    ['xvp_password_sep', 'rw', 'str'],      // Separator for XVP password fields
+    ['touchButton', 'rw', 'int'],           // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
+    ['scale', 'rw', 'float'],               // Display area scale factor
+    ['viewport', 'rw', 'bool'],             // Use viewport clipping
     ['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
+    ['capabilities', 'ro', 'arr'],          // Supported capabilities
 
     // 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
+    ['onCredentialsRequired', 'rw', 'func'], // onCredentialsRequired(rfb, types): VNC credentials are 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
+    ['onCapabilities', 'rw', 'func']        // onCapabilities(rfb, caps): the supported capabilities has changed
 ]);
 
 RFB.prototype.set_local_cursor = function (cursor) {
@@ -1495,8 +1485,7 @@ RFB.prototype.set_local_cursor = function (cursor) {
 
     // Need to send an updated list of encodings if we are connected
     if (this._rfb_connection_state === "connected") {
-        RFB.messages.clientEncodings(this._sock, this._encodings, cursor,
-                                     this._true_color);
+        this._sendEncodings();
     }
 };
 
@@ -1515,9 +1504,29 @@ RFB.prototype.set_view_only = function (view_only) {
     }
 };
 
-RFB.prototype.get_display = function () { return this._display; };
-RFB.prototype.get_keyboard = function () { return this._keyboard; };
-RFB.prototype.get_mouse = function () { return this._mouse; };
+RFB.prototype.set_touchButton = function (button) {
+    this._mouse.set_touchButton(button);
+};
+
+RFB.prototype.get_touchButton = function () {
+    return this._mouse.get_touchButton();
+};
+
+RFB.prototype.set_scale = function (scale) {
+    this._display.set_scale(scale);
+};
+
+RFB.prototype.get_scale = function () {
+    return this._display.get_scale();
+};
+
+RFB.prototype.set_viewport = function (viewport) {
+    this._display.set_viewport(viewport);
+};
+
+RFB.prototype.get_viewport = function () {
+    return this._display.get_viewport();
+};
 
 // Class Methods
 RFB.messages = {
@@ -1703,33 +1712,45 @@ RFB.messages = {
         sock.flush();
     },
 
-    pixelFormat: function (sock, bpp, depth, true_color) {
+    pixelFormat: function (sock, depth, true_color) {
         var buff = sock._sQ;
         var offset = sock._sQlen;
 
+        var bpp, bits;
+
+        if (depth > 16) {
+            bpp = 32;
+        } else if (depth > 8) {
+            bpp = 16;
+        } else {
+            bpp = 8;
+        }
+
+        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 * 8;             // bits-per-pixel
-        buff[offset + 5] = depth * 8;           // depth
+        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] = 255;  // red-max
+        buff[offset + 9] = (1 << bits) - 1;  // red-max
 
         buff[offset + 10] = 0;   // green-max
-        buff[offset + 11] = 255; // green-max
+        buff[offset + 11] = (1 << bits) - 1; // green-max
 
         buff[offset + 12] = 0;   // blue-max
-        buff[offset + 13] = 255; // blue-max
+        buff[offset + 13] = (1 << bits) - 1; // blue-max
 
-        buff[offset + 14] = 16;  // red-shift
-        buff[offset + 15] = 8;   // green-shift
-        buff[offset + 16] = 0;   // blue-shift
+        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
@@ -1739,36 +1760,26 @@ RFB.messages = {
         sock.flush();
     },
 
-    clientEncodings: function (sock, encodings, local_cursor, true_color) {
+    clientEncodings: function (sock, encodings) {
         var buff = sock._sQ;
         var offset = sock._sQlen;
 
         buff[offset] = 2; // msg-type
         buff[offset + 1] = 0; // padding
 
-        // offset + 2 and offset + 3 are encoding count
+        buff[offset + 2] = encodings.length >> 8;
+        buff[offset + 3] = encodings.length;
 
-        var i, j = offset + 4, cnt = 0;
+        var i, j = offset + 4;
         for (i = 0; i < encodings.length; i++) {
-            if (encodings[i][0] === "Cursor" && !local_cursor) {
-                Log.Debug("Skipping Cursor pseudo-encoding");
-            } else if (encodings[i][0] === "TIGHT" && !true_color) {
-                // TODO: remove this when we have tight+non-true-color
-                Log.Warn("Skipping tight as it is only supported with true color");
-            } else {
-                var enc = encodings[i][1];
-                buff[j] = enc >> 24;
-                buff[j + 1] = enc >> 16;
-                buff[j + 2] = enc >> 8;
-                buff[j + 3] = enc;
-
-                j += 4;
-                cnt++;
-            }
-        }
+            var enc = encodings[i];
+            buff[j] = enc >> 24;
+            buff[j + 1] = enc >> 16;
+            buff[j + 2] = enc >> 8;
+            buff[j + 3] = enc;
 
-        buff[offset + 2] = cnt >> 8;
-        buff[offset + 3] = cnt;
+            j += 4;
+        }
 
         sock._sQlen += j - offset;
         sock.flush();
@@ -1798,7 +1809,21 @@ RFB.messages = {
 
         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) {
@@ -1815,19 +1840,34 @@ RFB.encodingHandlers = {
             this._FBU.lines = this._FBU.height;
         }
 
-        this._FBU.bytes = this._FBU.width * this._fb_Bpp;  // at least a line
+        var pixelSize = this._fb_depth == 8 ? 1 : 4;
+        this._FBU.bytes = this._FBU.width * pixelSize;  // at least a line
         if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; }
         var cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines);
         var curr_height = Math.min(this._FBU.lines,
-                                   Math.floor(this._sock.rQlen() / (this._FBU.width * this._fb_Bpp)));
+                                   Math.floor(this._sock.rQlen() / (this._FBU.width * pixelSize)));
+        var data = this._sock.get_rQ();
+        var index = this._sock.get_rQi();
+        if (this._fb_depth == 8) {
+            var pixels = this._FBU.width * curr_height
+            var newdata = new Uint8Array(pixels * 4);
+            var i;
+            for (i = 0;i < pixels;i++) {
+                newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3;
+                newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3;
+                newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3;
+                newdata[i * 4 + 4] = 0;
+            }
+            data = newdata;
+            index = 0;
+        }
         this._display.blitImage(this._FBU.x, cur_y, this._FBU.width,
-                                curr_height, this._sock.get_rQ(),
-                                this._sock.get_rQi());
-        this._sock.rQskipBytes(this._FBU.width * curr_height * this._fb_Bpp);
+                                curr_height, data, index);
+        this._sock.rQskipBytes(this._FBU.width * curr_height * pixelSize);
         this._FBU.lines -= curr_height;
 
         if (this._FBU.lines > 0) {
-            this._FBU.bytes = this._FBU.width * this._fb_Bpp;  // At least another line
+            this._FBU.bytes = this._FBU.width * pixelSize;  // At least another line
         } else {
             this._FBU.rects--;
             this._FBU.bytes = 0;
@@ -1851,15 +1891,15 @@ RFB.encodingHandlers = {
     RRE: function () {
         var color;
         if (this._FBU.subrects === 0) {
-            this._FBU.bytes = 4 + this._fb_Bpp;
-            if (this._sock.rQwait("RRE", 4 + this._fb_Bpp)) { return false; }
+            this._FBU.bytes = 4 + 4;
+            if (this._sock.rQwait("RRE", 4 + 4)) { return false; }
             this._FBU.subrects = this._sock.rQshift32();
-            color = this._sock.rQshiftBytes(this._fb_Bpp);  // Background
+            color = this._sock.rQshiftBytes(4);  // Background
             this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color);
         }
 
-        while (this._FBU.subrects > 0 && this._sock.rQlen() >= (this._fb_Bpp + 8)) {
-            color = this._sock.rQshiftBytes(this._fb_Bpp);
+        while (this._FBU.subrects > 0 && this._sock.rQlen() >= (4 + 8)) {
+            color = this._sock.rQshiftBytes(4);
             var x = this._sock.rQshift16();
             var y = this._sock.rQshift16();
             var width = this._sock.rQshift16();
@@ -1870,7 +1910,7 @@ RFB.encodingHandlers = {
 
         if (this._FBU.subrects > 0) {
             var chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects);
-            this._FBU.bytes = (this._fb_Bpp + 8) * chunk;
+            this._FBU.bytes = (4 + 8) * chunk;
         } else {
             this._FBU.rects--;
             this._FBU.bytes = 0;
@@ -1911,20 +1951,20 @@ RFB.encodingHandlers = {
 
             // Figure out how much we are expecting
             if (subencoding & 0x01) {  // Raw
-                this._FBU.bytes += w * h * this._fb_Bpp;
+                this._FBU.bytes += w * h * 4;
             } else {
                 if (subencoding & 0x02) {  // Background
-                    this._FBU.bytes += this._fb_Bpp;
+                    this._FBU.bytes += 4;
                 }
                 if (subencoding & 0x04) {  // Foreground
-                    this._FBU.bytes += this._fb_Bpp;
+                    this._FBU.bytes += 4;
                 }
                 if (subencoding & 0x08) {  // AnySubrects
                     this._FBU.bytes++;  // Since we aren't shifting it off
                     if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; }
                     subrects = rQ[rQi + this._FBU.bytes - 1];  // Peek
                     if (subencoding & 0x10) {  // SubrectsColoured
-                        this._FBU.bytes += subrects * (this._fb_Bpp + 2);
+                        this._FBU.bytes += subrects * (4 + 2);
                     } else {
                         this._FBU.bytes += subrects * 2;
                     }
@@ -1948,22 +1988,12 @@ RFB.encodingHandlers = {
                 rQi += this._FBU.bytes - 1;
             } else {
                 if (this._FBU.subencoding & 0x02) {  // Background
-                    if (this._fb_Bpp == 1) {
-                        this._FBU.background = rQ[rQi];
-                    } else {
-                        // fb_Bpp is 4
-                        this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
-                    }
-                    rQi += this._fb_Bpp;
+                    this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
+                    rQi += 4;
                 }
                 if (this._FBU.subencoding & 0x04) {  // Foreground
-                    if (this._fb_Bpp == 1) {
-                        this._FBU.foreground = rQ[rQi];
-                    } else {
-                        // this._fb_Bpp is 4
-                        this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
-                    }
-                    rQi += this._fb_Bpp;
+                    this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
+                    rQi += 4;
                 }
 
                 this._display.startTile(x, y, w, h, this._FBU.background);
@@ -1974,13 +2004,8 @@ RFB.encodingHandlers = {
                     for (var s = 0; s < subrects; s++) {
                         var color;
                         if (this._FBU.subencoding & 0x10) {  // SubrectsColoured
-                            if (this._fb_Bpp === 1) {
-                                color = rQ[rQi];
-                            } else {
-                                // _fb_Bpp is 4
-                                color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
-                            }
-                            rQi += this._fb_Bpp;
+                            color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]];
+                            rQi += 4;
                         } else {
                             color = this._FBU.foreground;
                         }
@@ -2012,27 +2037,7 @@ RFB.encodingHandlers = {
         return true;
     },
 
-    getTightCLength: function (arr) {
-        var header = 1, data = 0;
-        data += arr[0] & 0x7f;
-        if (arr[0] & 0x80) {
-            header++;
-            data += (arr[1] & 0x7f) << 7;
-            if (arr[1] & 0x80) {
-                header++;
-                data += arr[2] << 14;
-            }
-        }
-        return [header, data];
-    },
-
-    display_tight: function (isTightPNG) {
-        if (this._fb_depth === 1) {
-            this._fail("Internal error",
-                       "Tight protocol handler only implements " +
-                       "true color mode");
-        }
-
+    TIGHT: function () {
         this._FBU.bytes = 1;  // compression-control byte
         if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; }
 
@@ -2148,7 +2153,7 @@ RFB.encodingHandlers = {
 
         var handlePalette = function () {
             var numColors = rQ[rQi + 2] + 1;
-            var paletteSize = numColors * this._fb_depth;
+            var paletteSize = numColors * 3;
             this._FBU.bytes += paletteSize;
             if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; }
 
@@ -2208,7 +2213,7 @@ RFB.encodingHandlers = {
 
         var handleCopy = function () {
             var raw = false;
-            var uncompressedSize = this._FBU.width * this._FBU.height * this._fb_depth;
+            var uncompressedSize = this._FBU.width * this._FBU.height * 3;
             if (uncompressedSize < 12) {
                 raw = true;
                 cl_header = 0;
@@ -2264,15 +2269,10 @@ RFB.encodingHandlers = {
                                "Illegal tight compression received, " +
                                "ctl: " + ctl);
 
-        if (isTightPNG && (cmode === "filter" || cmode === "copy")) {
-            return this._fail("Unexpected server message",
-                              "filter/copy received in tightPNG mode");
-        }
-
         switch (cmode) {
-            // fill use fb_depth because TPIXELs drop the padding byte
+            // fill use depth because TPIXELs drop the padding byte
             case "fill":  // TPIXEL
-                this._FBU.bytes += this._fb_depth;
+                this._FBU.bytes += 3;
                 break;
             case "jpeg":  // max clength
                 this._FBU.bytes += 3;
@@ -2344,33 +2344,18 @@ RFB.encodingHandlers = {
         return true;
     },
 
-    TIGHT: function () { return this._encHandlers.display_tight(false); },
-    TIGHT_PNG: function () { return this._encHandlers.display_tight(true); },
-
     last_rect: function () {
         this._FBU.rects = 0;
         return true;
     },
 
-    handle_FB_resize: function () {
-        this._fb_width = this._FBU.width;
-        this._fb_height = this._FBU.height;
-        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);
-        this._timing.fbu_rt_start = (new Date()).getTime();
-        this._updateContinuousUpdates();
-
-        this._FBU.bytes = 0;
-        this._FBU.rects -= 1;
-        return true;
-    },
-
     ExtendedDesktopSize: function () {
         this._FBU.bytes = 1;
         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);
@@ -2421,15 +2406,19 @@ RFB.encodingHandlers = {
             }
             this._notification("Server did not accept the resize request: "
                                + msg, 'normal');
-            return true;
+        } else {
+            this._resize(this._FBU.width, this._FBU.height);
         }
 
-        this._encHandlers.handle_FB_resize();
+        this._FBU.bytes = 0;
+        this._FBU.rects -= 1;
         return true;
     },
 
     DesktopSize: function () {
-        this._encHandlers.handle_FB_resize();
+        this._resize(this._FBU.width, this._FBU.height);
+        this._FBU.bytes = 0;
+        this._FBU.rects -= 1;
         return true;
     },
 
@@ -2440,7 +2429,7 @@ RFB.encodingHandlers = {
         var w = this._FBU.width;
         var h = this._FBU.height;
 
-        var pixelslength = w * h * this._fb_Bpp;
+        var pixelslength = w * h * 4;
         var masklength = Math.floor((w + 7) / 8) * h;
 
         this._FBU.bytes = pixelslength + masklength;
@@ -2460,17 +2449,13 @@ RFB.encodingHandlers = {
     QEMUExtendedKeyEvent: function () {
         this._FBU.rects--;
 
-        var keyboardEvent = document.createEvent("keyboardEvent");
-        if (keyboardEvent.code !== undefined) {
-            this._qemuExtKeyEventSupported = true;
+        // Old Safari doesn't support creating keyboard events
+        try {
+            var keyboardEvent = document.createEvent("keyboardEvent");
+            if (keyboardEvent.code !== undefined) {
+                this._qemuExtKeyEventSupported = true;
+            }
+        } catch (err) {
         }
     },
-
-    JPEG_quality_lo: function () {
-        Log.Error("Server sent jpeg_quality pseudo-encoding");
-    },
-
-    compress_lo: function () {
-        Log.Error("Server sent compress level pseudo-encoding");
-    }
 };