From 430f00d6fefbaedf72a609f5d747eed648cf843c Mon Sep 17 00:00:00 2001 From: Pierre Ossman Date: Fri, 13 Oct 2017 13:50:49 +0200 Subject: [PATCH] Allow other credentials than just password Makes the XVP authentication mechanism more general. --- app/ui.js | 14 ++++----- core/rfb.js | 41 ++++++++++++-------------- docs/API.md | 73 ++++++++++++++++++++++++++++------------------- tests/playback.js | 4 +-- tests/test.rfb.js | 58 +++++++++++++++++++------------------ vnc_lite.html | 13 ++++----- 6 files changed, 105 insertions(+), 98 deletions(-) diff --git a/app/ui.js b/app/ui.js index c5cc7cb..6e08172 100644 --- a/app/ui.js +++ b/app/ui.js @@ -206,7 +206,7 @@ var UI = { 'onNotification': UI.notification, 'onUpdateState': UI.updateState, 'onDisconnected': UI.disconnectFinished, - 'onPasswordRequired': UI.passwordRequired, + 'onCredentialsRequired': UI.credentials, 'onXvpInit': UI.updateXvpButton, 'onClipboard': UI.clipboardReceive, 'onBell': UI.bell, @@ -1067,7 +1067,7 @@ var UI = { UI.updateLocalCursor(); UI.updateViewOnly(); - UI.rfb.connect(host, port, password, path); + UI.rfb.connect(host, port, { password: password }, path); }, disconnect: function() { @@ -1127,8 +1127,8 @@ var UI = { * PASSWORD * ------v------*/ - passwordRequired: function(rfb, msg) { - + credentials: function(rfb, types) { + // FIXME: handle more types document.getElementById('noVNC_password_dlg') .classList.add('noVNC_open'); @@ -1136,9 +1136,7 @@ var UI = { 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"); }, @@ -1148,7 +1146,7 @@ var UI = { 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'); diff --git a/core/rfb.js b/core/rfb.js index d4f3f2d..3565f7c 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -36,7 +36,7 @@ export default function RFB(defaults) { this._rfb_host = ''; this._rfb_port = 5900; - this._rfb_password = ''; + this._rfb_credentials = {}; this._rfb_path = ''; this._rfb_connection_state = ''; @@ -124,7 +124,6 @@ export default function RFB(defaults) { '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 @@ -134,7 +133,7 @@ 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 @@ -241,10 +240,10 @@ export default function RFB(defaults) { RFB.prototype = { // Public methods - connect: function (host, port, password, path) { + connect: function (host, port, creds, path) { this._rfb_host = host; this._rfb_port = port; - this._rfb_password = (password !== undefined) ? password : ""; + this._rfb_credentials = (creds !== undefined) ? creds : {}; this._rfb_path = (path !== undefined) ? path : ""; if (!this._rfb_host) { @@ -264,8 +263,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); }, @@ -848,21 +847,18 @@ 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(); }, @@ -870,14 +866,14 @@ RFB.prototype = { _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) { + this._onCredentialsRequired(this, ["password"]); 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; @@ -1496,7 +1492,6 @@ make_properties(RFB, [ ['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 - ['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 @@ -1506,7 +1501,7 @@ make_properties(RFB, [ ['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 diff --git a/docs/API.md b/docs/API.md index 959d137..9052daa 100644 --- a/docs/API.md +++ b/docs/API.md @@ -37,7 +37,6 @@ attribute mode is one of the following: | touchButton | int | RW | 1 | Button mask (1, 2, 4) for which click to send on touch devices. 0 means ignore clicks. | scale | float | RW | 1.0 | Display area scale factor | viewport | bool | RW | false | Use viewport clipping -| 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 @@ -50,22 +49,22 @@ In addition to the getter and setter methods to modify configuration attributes, the RFB object has other methods that are available in the object instance. -| 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 -| autoscale | (width, height, downscaleOnly) | Scale the display -| clippingDisplay | () | Check if the remote display is larger than the client display -| requestDesktopSize | (width, height) | Send a request to change the remote desktop size. -| viewportChangeSize | (width, height) | Change size of the viewport +| name | parameters | description +| ------------------ | ------------------------------- | ------------ +| connect | (host, port, credentials, path) | Connect to the given host:port/path. Optional credentials and path. +| disconnect | () | Disconnect +| sendCredentials | (credentials) | Send credentials after onCredentialsRequired 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 +| autoscale | (width, height, downscaleOnly) | Scale the display +| clippingDisplay | () | Check if the remote display is larger than the client display +| requestDesktopSize | (width, height) | Send a request to change the remote desktop size. +| viewportChangeSize | (width, height) | Change size of the viewport ## 3 Callbacks @@ -73,19 +72,19 @@ object instance. The RFB object has certain events that can be hooked with callback functions. -| 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. +| 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. +| onCredentialsRequired | (rfb, types) | VNC credentials are required (use sendCredentials) +| 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__ @@ -103,6 +102,20 @@ created for new connections. | disconnecting | starting to disconnect | disconnected | disconnected - permanent end-state for this RFB object +__RFB onCredentialsRequired callback details__ + +The onCredentialsRequired callback is called when the server requests more +credentials than was specified to connect(). The types argument is a list +of all the credentials that are required. Currently the following are +defined: + +| name | description +| -------- | ------------ +| username | User that authenticates +| password | Password for user +| target | String specifying target machine or session + + __RFB onFBUReceive and on FBUComplete callback details__ The onFBUReceive callback is invoked when a frame buffer update diff --git a/tests/playback.js b/tests/playback.js index 745e1f5..ac36ee4 100644 --- a/tests/playback.js +++ b/tests/playback.js @@ -104,10 +104,10 @@ RecordingPlayer.prototype = { this._rfb._sock.close = function () {}; this._rfb._sock.flush = function () {}; this._rfb._checkEvents = function () {}; - this._rfb.connect = function (host, port, password, path) { + this._rfb.connect = function (host, port, credentials, path) { this._rfb_host = host; this._rfb_port = port; - this._rfb_password = (password !== undefined) ? password : ""; + this._rfb_credentials = {}; this._rfb_path = (path !== undefined) ? path : ""; this._sock.init('binary', 'ws'); this._rfb_connection_state = 'connecting'; diff --git a/tests/test.rfb.js b/tests/test.rfb.js index fa6ba56..e4336f6 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -116,18 +116,18 @@ describe('Remote Frame Buffer Protocol Client', function() { }); }); - describe('#sendPassword', function () { + describe('#sendCredentials', function () { beforeEach(function () { this.clock = sinon.useFakeTimers(); }); afterEach(function () { this.clock.restore(); }); - it('should set the rfb password properly"', function () { - client.sendPassword('pass'); - expect(client._rfb_password).to.equal('pass'); + it('should set the rfb credentials properly"', function () { + client.sendCredentials({ password: 'pass' }); + expect(client._rfb_credentials).to.deep.equal({ password: 'pass' }); }); it('should call init_msg "soon"', function () { client._init_msg = sinon.spy(); - client.sendPassword('pass'); + client.sendCredentials({ password: 'pass' }); this.clock.tick(5); expect(client._init_msg).to.have.been.calledOnce; }); @@ -836,21 +836,22 @@ describe('Remote Frame Buffer Protocol Client', function() { client._rfb_version = 3.8; }); - it('should call the passwordRequired callback if missing a password', function () { - client.set_onPasswordRequired(sinon.spy()); + it('should call the onCredentialsRequired callback if missing a password', function () { + client.set_onCredentialsRequired(sinon.spy()); send_security(2, client); var challenge = []; for (var i = 0; i < 16; i++) { challenge[i] = i; } client._sock._websocket._receive_data(new Uint8Array(challenge)); - var spy = client.get_onPasswordRequired(); - expect(client._rfb_password.length).to.equal(0); + var spy = client.get_onCredentialsRequired(); + expect(client._rfb_credentials).to.be.empty; expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.have.members(["password"]); }); it('should encrypt the password with DES and then send it back', function () { - client._rfb_password = 'passwd'; + client._rfb_credentials = { password: 'passwd' }; send_security(2, client); client._sock._websocket._get_sent_data(); // skip the choice of auth reply @@ -863,7 +864,7 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should transition to SecurityResult immediately after sending the password', function () { - client._rfb_password = 'passwd'; + client._rfb_credentials = { password: 'passwd' }; send_security(2, client); var challenge = []; @@ -886,41 +887,44 @@ describe('Remote Frame Buffer Protocol Client', function() { }); it('should fall through to standard VNC authentication upon completion', function () { - client.set_xvp_password_sep('#'); - client._rfb_password = 'user#target#password'; + client._rfb_credentials = { username: 'user', + target: 'target', + password: 'password' }; client._negotiate_std_vnc_auth = sinon.spy(); send_security(22, client); expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; }); - it('should call the passwordRequired callback if the password is missing', function() { - client.set_onPasswordRequired(sinon.spy()); - client._rfb_password = ''; + it('should call the onCredentialsRequired callback if all credentials are missing', function() { + client.set_onCredentialsRequired(sinon.spy()); + client._rfb_credentials = {}; send_security(22, client); - var spy = client.get_onPasswordRequired(); - expect(client._rfb_password.length).to.equal(0); + var spy = client.get_onCredentialsRequired(); + expect(client._rfb_credentials).to.be.empty; expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.have.members(["username", "password", "target"]); }); - it('should call the passwordRequired callback if the password is improperly formatted', function() { - client.set_onPasswordRequired(sinon.spy()); - client._rfb_password = 'user@target'; + it('should call the onCredentialsRequired callback if some credentials are missing', function() { + client.set_onCredentialsRequired(sinon.spy()); + client._rfb_credentials = { username: 'user', + target: 'target' }; send_security(22, client); - var spy = client.get_onPasswordRequired(); + var spy = client.get_onCredentialsRequired(); expect(spy).to.have.been.calledOnce; + expect(spy.args[0][1]).to.have.members(["username", "password", "target"]); }); - it('should split the password, send the first two parts, and pass on the last part', function () { - client.set_xvp_password_sep('#'); - client._rfb_password = 'user#target#password'; + it('should send user and target separately', function () { + client._rfb_credentials = { username: 'user', + target: 'target', + password: 'password' }; client._negotiate_std_vnc_auth = sinon.spy(); send_security(22, client); - expect(client._rfb_password).to.equal('password'); - var expected = [22, 4, 6]; // auth selection, len user, len target for (var i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); } diff --git a/vnc_lite.html b/vnc_lite.html index f54cb13..73aa426 100644 --- a/vnc_lite.html +++ b/vnc_lite.html @@ -99,10 +99,7 @@ function updateDesktopName(rfb, name) { desktopName = name; } - function passwordRequired(rfb, msg) { - if (typeof msg === 'undefined') { - msg = 'Password Required: '; - } + function credentials(rfb, types) { var html; var form = document.createElement('form'); @@ -115,10 +112,10 @@ document.getElementById('noVNC_status_bar').setAttribute("class", "noVNC_status_warn"); document.getElementById('noVNC_status').innerHTML = ''; document.getElementById('noVNC_status').appendChild(form); - document.getElementById('noVNC_status').querySelector('label').textContent = msg; + document.getElementById('noVNC_status').querySelector('label').textContent = 'Password Required: '; } function setPassword() { - rfb.sendPassword(document.getElementById('password_input').value); + rfb.sendCredentials({ password: document.getElementById('password_input').value }); return false; } function sendCtrlAltDel() { @@ -266,7 +263,7 @@ 'onUpdateState': updateState, 'onDisconnected': disconnected, 'onXvpInit': xvpInit, - 'onPasswordRequired': passwordRequired, + 'onCredentialsRequired': credentials, 'onFBUComplete': FBUComplete, 'onDesktopName': updateDesktopName}); } catch (exc) { @@ -274,7 +271,7 @@ return; // don't continue trying to connect } - rfb.connect(host, port, password, path); + rfb.connect(host, port, { password: password }, path); })(); -- 2.39.5