]> git.proxmox.com Git - mirror_novnc.git/blobdiff - core/rfb.js
Switch to URL for connect()
[mirror_novnc.git] / core / rfb.js
index a4b5417e177bc969b6576638a523d48b9f19038e..ec413a84f74140eaae7691bf036a121e4d6184f5 100644 (file)
@@ -34,40 +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;
 
-    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 +95,11 @@ export default function RFB(defaults) {
         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 +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 = [];
@@ -114,18 +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
         '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
@@ -135,19 +141,22 @@ 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, 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
+        'onCapabilities': function () { }       // onCapabilities(rfb, caps): the supported capabilities has changed
     });
 
     // main setup
     Log.Debug(">> RFB.constructor");
 
+    // 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);
@@ -170,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,
@@ -226,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);
 
@@ -237,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) {
-            return this._fail(
-                _("Must set host"));
+        if (!url) {
+            this._fail(_("Must specify URL"));
+            return;
         }
 
         this._rfb_init_state = '';
@@ -260,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);
     },
 
@@ -279,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
@@ -334,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) {
@@ -357,26 +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;
-        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, this._wsProtocols);
         } catch (e) {
             if (e.name === 'SyntaxError') {
                 this._fail("Invalid host or port value given", e);
@@ -385,40 +384,23 @@ 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 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;
 
@@ -442,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
@@ -612,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");
@@ -816,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;
@@ -1254,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",
@@ -1369,12 +1360,6 @@ 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': encodingName(this._FBU.encoding)});
-
                 if (!this._encHandlers[this._FBU.encoding]) {
                     this._fail("Unexpected server message",
                                "Unsupported encoding " +
@@ -1429,8 +1414,6 @@ RFB.prototype = {
 
         this._display.flip();
 
-        this._onFBUComplete(this);
-
         return true;  // We finished this FBU
     },
 
@@ -1452,34 +1435,39 @@ RFB.prototype = {
 
         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
     ['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) {
@@ -1516,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 = {
@@ -1801,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) {
@@ -2332,6 +2354,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);