Allow other credentials than just password

Makes the XVP authentication mechanism more general.
This commit is contained in:
Pierre Ossman 2017-10-13 13:50:49 +02:00
parent 233c8b6a53
commit 430f00d6fe
6 changed files with 105 additions and 98 deletions

View File

@ -206,7 +206,7 @@ var UI = {
'onNotification': UI.notification, 'onNotification': UI.notification,
'onUpdateState': UI.updateState, 'onUpdateState': UI.updateState,
'onDisconnected': UI.disconnectFinished, 'onDisconnected': UI.disconnectFinished,
'onPasswordRequired': UI.passwordRequired, 'onCredentialsRequired': UI.credentials,
'onXvpInit': UI.updateXvpButton, 'onXvpInit': UI.updateXvpButton,
'onClipboard': UI.clipboardReceive, 'onClipboard': UI.clipboardReceive,
'onBell': UI.bell, 'onBell': UI.bell,
@ -1067,7 +1067,7 @@ var UI = {
UI.updateLocalCursor(); UI.updateLocalCursor();
UI.updateViewOnly(); UI.updateViewOnly();
UI.rfb.connect(host, port, password, path); UI.rfb.connect(host, port, { password: password }, path);
}, },
disconnect: function() { disconnect: function() {
@ -1127,8 +1127,8 @@ var UI = {
* PASSWORD * PASSWORD
* ------v------*/ * ------v------*/
passwordRequired: function(rfb, msg) { credentials: function(rfb, types) {
// FIXME: handle more types
document.getElementById('noVNC_password_dlg') document.getElementById('noVNC_password_dlg')
.classList.add('noVNC_open'); .classList.add('noVNC_open');
@ -1136,9 +1136,7 @@ var UI = {
document.getElementById('noVNC_password_input').focus(); document.getElementById('noVNC_password_input').focus();
}, 100); }, 100);
if (typeof msg === 'undefined') { var msg = _("Password is required");
msg = _("Password is required");
}
Log.Warn(msg); Log.Warn(msg);
UI.showStatus(msg, "warning"); UI.showStatus(msg, "warning");
}, },
@ -1148,7 +1146,7 @@ var UI = {
var password = inputElem.value; var password = inputElem.value;
// Clear the input after reading the password // Clear the input after reading the password
inputElem.value = ""; inputElem.value = "";
UI.rfb.sendPassword(password); UI.rfb.sendCredentials({ password: password });
UI.reconnect_password = password; UI.reconnect_password = password;
document.getElementById('noVNC_password_dlg') document.getElementById('noVNC_password_dlg')
.classList.remove('noVNC_open'); .classList.remove('noVNC_open');

View File

@ -36,7 +36,7 @@ export default function RFB(defaults) {
this._rfb_host = ''; this._rfb_host = '';
this._rfb_port = 5900; this._rfb_port = 5900;
this._rfb_password = ''; this._rfb_credentials = {};
this._rfb_path = ''; this._rfb_path = '';
this._rfb_connection_state = ''; this._rfb_connection_state = '';
@ -124,7 +124,6 @@ export default function RFB(defaults) {
'local_cursor': false, // Request locally rendered cursor 'local_cursor': false, // Request locally rendered cursor
'shared': true, // Request shared mode 'shared': true, // Request shared mode
'view_only': false, // Disable client mouse/keyboard 'view_only': false, // Disable client mouse/keyboard
'xvp_password_sep': '@', // Separator for XVP password fields
'disconnectTimeout': 3, // Time (s) to wait for disconnection 'disconnectTimeout': 3, // Time (s) to wait for disconnection
'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection 'wsProtocols': ['binary'], // Protocols to use in the WebSocket connection
'repeaterID': '', // [UltraVNC] RepeaterID to connect to 'repeaterID': '', // [UltraVNC] RepeaterID to connect to
@ -134,7 +133,7 @@ export default function RFB(defaults) {
'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate): connection state change 'onUpdateState': function () { }, // onUpdateState(rfb, state, oldstate): connection state change
'onNotification': function () { }, // onNotification(rfb, msg, level, options): notification for UI 'onNotification': function () { }, // onNotification(rfb, msg, level, options): notification for UI
'onDisconnected': function () { }, // onDisconnected(rfb, reason): disconnection finished '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 'onClipboard': function () { }, // onClipboard(rfb, text): RFB clipboard contents received
'onBell': function () { }, // onBell(rfb): RFB Bell message received 'onBell': function () { }, // onBell(rfb): RFB Bell message received
'onFBUReceive': function () { }, // onFBUReceive(rfb, rect): RFB FBU rect received but not yet processed 'onFBUReceive': function () { }, // onFBUReceive(rfb, rect): RFB FBU rect received but not yet processed
@ -241,10 +240,10 @@ export default function RFB(defaults) {
RFB.prototype = { RFB.prototype = {
// Public methods // Public methods
connect: function (host, port, password, path) { connect: function (host, port, creds, path) {
this._rfb_host = host; this._rfb_host = host;
this._rfb_port = port; this._rfb_port = port;
this._rfb_password = (password !== undefined) ? password : ""; this._rfb_credentials = (creds !== undefined) ? creds : {};
this._rfb_path = (path !== undefined) ? path : ""; this._rfb_path = (path !== undefined) ? path : "";
if (!this._rfb_host) { if (!this._rfb_host) {
@ -264,8 +263,8 @@ RFB.prototype = {
this._sock.off('open'); this._sock.off('open');
}, },
sendPassword: function (passwd) { sendCredentials: function (creds) {
this._rfb_password = passwd; this._rfb_credentials = creds;
setTimeout(this._init_msg.bind(this), 0); setTimeout(this._init_msg.bind(this), 0);
}, },
@ -848,21 +847,18 @@ RFB.prototype = {
// authentication // authentication
_negotiate_xvp_auth: function () { _negotiate_xvp_auth: function () {
var xvp_sep = this._xvp_password_sep; if (!this._rfb_credentials.username ||
var xvp_auth = this._rfb_password.split(xvp_sep); !this._rfb_credentials.password ||
if (xvp_auth.length < 3) { !this._rfb_credentials.target) {
var msg = 'XVP credentials required (user' + xvp_sep + this._onCredentialsRequired(this, ["username", "password", "target"]);
'target' + xvp_sep + 'password) -- got only ' + this._rfb_password;
this._onPasswordRequired(this, msg);
return false; return false;
} }
var xvp_auth_str = String.fromCharCode(xvp_auth[0].length) + var xvp_auth_str = String.fromCharCode(this._rfb_credentials.username.length) +
String.fromCharCode(xvp_auth[1].length) + String.fromCharCode(this._rfb_credentials.target.length) +
xvp_auth[0] + this._rfb_credentials.username +
xvp_auth[1]; this._rfb_credentials.target;
this._sock.send_string(xvp_auth_str); this._sock.send_string(xvp_auth_str);
this._rfb_password = xvp_auth.slice(2).join(xvp_sep);
this._rfb_auth_scheme = 2; this._rfb_auth_scheme = 2;
return this._negotiate_authentication(); return this._negotiate_authentication();
}, },
@ -870,14 +866,14 @@ RFB.prototype = {
_negotiate_std_vnc_auth: function () { _negotiate_std_vnc_auth: function () {
if (this._sock.rQwait("auth challenge", 16)) { return false; } if (this._sock.rQwait("auth challenge", 16)) { return false; }
if (this._rfb_password.length === 0) { if (!this._rfb_credentials.password) {
this._onPasswordRequired(this); this._onCredentialsRequired(this, ["password"]);
return false; return false;
} }
// TODO(directxman12): make genDES not require an Array // TODO(directxman12): make genDES not require an Array
var challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); 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._sock.send(response);
this._rfb_init_state = "SecurityResult"; this._rfb_init_state = "SecurityResult";
return true; return true;
@ -1496,7 +1492,6 @@ make_properties(RFB, [
['touchButton', 'rw', 'int'], // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) ['touchButton', 'rw', 'int'], // Button mask (1, 2, 4) for touch devices (0 means ignore clicks)
['scale', 'rw', 'float'], // Display area scale factor ['scale', 'rw', 'float'], // Display area scale factor
['viewport', 'rw', 'bool'], // Use viewport clipping ['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 ['disconnectTimeout', 'rw', 'int'], // Time (s) to wait for disconnection
['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection ['wsProtocols', 'rw', 'arr'], // Protocols to use in the WebSocket connection
['repeaterID', 'rw', 'str'], // [UltraVNC] RepeaterID to connect to ['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 ['onUpdateState', 'rw', 'func'], // onUpdateState(rfb, state, oldstate): connection state change
['onNotification', 'rw', 'func'], // onNotification(rfb, msg, level, options): notification for the UI ['onNotification', 'rw', 'func'], // onNotification(rfb, msg, level, options): notification for the UI
['onDisconnected', 'rw', 'func'], // onDisconnected(rfb, reason): disconnection finished ['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 ['onClipboard', 'rw', 'func'], // onClipboard(rfb, text): RFB clipboard contents received
['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received ['onBell', 'rw', 'func'], // onBell(rfb): RFB Bell message received
['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed ['onFBUReceive', 'rw', 'func'], // onFBUReceive(rfb, fbu): RFB FBU received but not yet processed

View File

@ -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. | 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 | scale | float | RW | 1.0 | Display area scale factor
| viewport | bool | RW | false | Use viewport clipping | 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 | disconnectTimeout | int | RW | 3 | Time (in seconds) to wait for disconnection
| wsProtocols | arr | RW | ['binary'] | Protocols to use in the WebSocket connection | wsProtocols | arr | RW | ['binary'] | Protocols to use in the WebSocket connection
| repeaterID | str | RW | '' | UltraVNC RepeaterID to connect to | repeaterID | str | RW | '' | UltraVNC RepeaterID to connect to
@ -51,10 +50,10 @@ attributes, the RFB object has other methods that are available in the
object instance. object instance.
| name | parameters | description | name | parameters | description
| ------------------ | ------------------------------ | ------------ | ------------------ | ------------------------------- | ------------
| connect | (host, port, password, path) | Connect to the given host:port/path. Optional password and path. | connect | (host, port, credentials, path) | Connect to the given host:port/path. Optional credentials and path.
| disconnect | () | Disconnect | disconnect | () | Disconnect
| sendPassword | (passwd) | Send password after onPasswordRequired callback | sendCredentials | (credentials) | Send credentials after onCredentialsRequired callback
| sendCtrlAltDel | () | Send Ctrl-Alt-Del key sequence | sendCtrlAltDel | () | Send Ctrl-Alt-Del key sequence
| xvpOp | (ver, op) | Send a XVP operation (2=shutdown, 3=reboot, 4=reset) | xvpOp | (ver, op) | Send a XVP operation (2=shutdown, 3=reboot, 4=reset)
| xvpShutdown | () | Send XVP shutdown. | xvpShutdown | () | Send XVP shutdown.
@ -74,11 +73,11 @@ The RFB object has certain events that can be hooked with callback
functions. functions.
| name | parameters | description | name | parameters | description
| ------------------ | -------------------------- | ------------ | --------------------- | -------------------------- | ------------
| onUpdateState | (rfb, state, oldstate) | Connection state change (see details below) | onUpdateState | (rfb, state, oldstate) | Connection state change (see details below)
| onNotification | (rfb, msg, level, options) | Notification for the UI (optional options) | 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. | 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. | onCredentialsRequired | (rfb, types) | VNC credentials are required (use sendCredentials)
| onClipboard | (rfb, text) | RFB clipboard contents received | onClipboard | (rfb, text) | RFB clipboard contents received
| onBell | (rfb) | RFB Bell message received | onBell | (rfb) | RFB Bell message received
| onFBUReceive | (rfb, fbu) | RFB FBU received but not yet processed (see details below) | onFBUReceive | (rfb, fbu) | RFB FBU received but not yet processed (see details below)
@ -103,6 +102,20 @@ created for new connections.
| disconnecting | starting to disconnect | disconnecting | starting to disconnect
| disconnected | disconnected - permanent end-state for this RFB object | 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__ __RFB onFBUReceive and on FBUComplete callback details__
The onFBUReceive callback is invoked when a frame buffer update The onFBUReceive callback is invoked when a frame buffer update

View File

@ -104,10 +104,10 @@ RecordingPlayer.prototype = {
this._rfb._sock.close = function () {}; this._rfb._sock.close = function () {};
this._rfb._sock.flush = function () {}; this._rfb._sock.flush = function () {};
this._rfb._checkEvents = 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_host = host;
this._rfb_port = port; this._rfb_port = port;
this._rfb_password = (password !== undefined) ? password : ""; this._rfb_credentials = {};
this._rfb_path = (path !== undefined) ? path : ""; this._rfb_path = (path !== undefined) ? path : "";
this._sock.init('binary', 'ws'); this._sock.init('binary', 'ws');
this._rfb_connection_state = 'connecting'; this._rfb_connection_state = 'connecting';

View File

@ -116,18 +116,18 @@ describe('Remote Frame Buffer Protocol Client', function() {
}); });
}); });
describe('#sendPassword', function () { describe('#sendCredentials', function () {
beforeEach(function () { this.clock = sinon.useFakeTimers(); }); beforeEach(function () { this.clock = sinon.useFakeTimers(); });
afterEach(function () { this.clock.restore(); }); afterEach(function () { this.clock.restore(); });
it('should set the rfb password properly"', function () { it('should set the rfb credentials properly"', function () {
client.sendPassword('pass'); client.sendCredentials({ password: 'pass' });
expect(client._rfb_password).to.equal('pass'); expect(client._rfb_credentials).to.deep.equal({ password: 'pass' });
}); });
it('should call init_msg "soon"', function () { it('should call init_msg "soon"', function () {
client._init_msg = sinon.spy(); client._init_msg = sinon.spy();
client.sendPassword('pass'); client.sendCredentials({ password: 'pass' });
this.clock.tick(5); this.clock.tick(5);
expect(client._init_msg).to.have.been.calledOnce; expect(client._init_msg).to.have.been.calledOnce;
}); });
@ -836,21 +836,22 @@ describe('Remote Frame Buffer Protocol Client', function() {
client._rfb_version = 3.8; client._rfb_version = 3.8;
}); });
it('should call the passwordRequired callback if missing a password', function () { it('should call the onCredentialsRequired callback if missing a password', function () {
client.set_onPasswordRequired(sinon.spy()); client.set_onCredentialsRequired(sinon.spy());
send_security(2, client); send_security(2, client);
var challenge = []; var challenge = [];
for (var i = 0; i < 16; i++) { challenge[i] = i; } for (var i = 0; i < 16; i++) { challenge[i] = i; }
client._sock._websocket._receive_data(new Uint8Array(challenge)); client._sock._websocket._receive_data(new Uint8Array(challenge));
var spy = client.get_onPasswordRequired(); var spy = client.get_onCredentialsRequired();
expect(client._rfb_password.length).to.equal(0); expect(client._rfb_credentials).to.be.empty;
expect(spy).to.have.been.calledOnce; 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 () { 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); send_security(2, client);
client._sock._websocket._get_sent_data(); // skip the choice of auth reply 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 () { it('should transition to SecurityResult immediately after sending the password', function () {
client._rfb_password = 'passwd'; client._rfb_credentials = { password: 'passwd' };
send_security(2, client); send_security(2, client);
var challenge = []; var challenge = [];
@ -886,41 +887,44 @@ describe('Remote Frame Buffer Protocol Client', function() {
}); });
it('should fall through to standard VNC authentication upon completion', function () { it('should fall through to standard VNC authentication upon completion', function () {
client.set_xvp_password_sep('#'); client._rfb_credentials = { username: 'user',
client._rfb_password = 'user#target#password'; target: 'target',
password: 'password' };
client._negotiate_std_vnc_auth = sinon.spy(); client._negotiate_std_vnc_auth = sinon.spy();
send_security(22, client); send_security(22, client);
expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce;
}); });
it('should call the passwordRequired callback if the password is missing', function() { it('should call the onCredentialsRequired callback if all credentials are missing', function() {
client.set_onPasswordRequired(sinon.spy()); client.set_onCredentialsRequired(sinon.spy());
client._rfb_password = ''; client._rfb_credentials = {};
send_security(22, client); send_security(22, client);
var spy = client.get_onPasswordRequired(); var spy = client.get_onCredentialsRequired();
expect(client._rfb_password.length).to.equal(0); expect(client._rfb_credentials).to.be.empty;
expect(spy).to.have.been.calledOnce; 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() { it('should call the onCredentialsRequired callback if some credentials are missing', function() {
client.set_onPasswordRequired(sinon.spy()); client.set_onCredentialsRequired(sinon.spy());
client._rfb_password = 'user@target'; client._rfb_credentials = { username: 'user',
target: 'target' };
send_security(22, client); send_security(22, client);
var spy = client.get_onPasswordRequired(); var spy = client.get_onCredentialsRequired();
expect(spy).to.have.been.calledOnce; 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 () { it('should send user and target separately', function () {
client.set_xvp_password_sep('#'); client._rfb_credentials = { username: 'user',
client._rfb_password = 'user#target#password'; target: 'target',
password: 'password' };
client._negotiate_std_vnc_auth = sinon.spy(); client._negotiate_std_vnc_auth = sinon.spy();
send_security(22, client); send_security(22, client);
expect(client._rfb_password).to.equal('password');
var expected = [22, 4, 6]; // auth selection, len user, len target var expected = [22, 4, 6]; // auth selection, len user, len target
for (var i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); } for (var i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); }

View File

@ -99,10 +99,7 @@
function updateDesktopName(rfb, name) { function updateDesktopName(rfb, name) {
desktopName = name; desktopName = name;
} }
function passwordRequired(rfb, msg) { function credentials(rfb, types) {
if (typeof msg === 'undefined') {
msg = 'Password Required: ';
}
var html; var html;
var form = document.createElement('form'); var form = document.createElement('form');
@ -115,10 +112,10 @@
document.getElementById('noVNC_status_bar').setAttribute("class", "noVNC_status_warn"); document.getElementById('noVNC_status_bar').setAttribute("class", "noVNC_status_warn");
document.getElementById('noVNC_status').innerHTML = ''; document.getElementById('noVNC_status').innerHTML = '';
document.getElementById('noVNC_status').appendChild(form); 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() { function setPassword() {
rfb.sendPassword(document.getElementById('password_input').value); rfb.sendCredentials({ password: document.getElementById('password_input').value });
return false; return false;
} }
function sendCtrlAltDel() { function sendCtrlAltDel() {
@ -266,7 +263,7 @@
'onUpdateState': updateState, 'onUpdateState': updateState,
'onDisconnected': disconnected, 'onDisconnected': disconnected,
'onXvpInit': xvpInit, 'onXvpInit': xvpInit,
'onPasswordRequired': passwordRequired, 'onCredentialsRequired': credentials,
'onFBUComplete': FBUComplete, 'onFBUComplete': FBUComplete,
'onDesktopName': updateDesktopName}); 'onDesktopName': updateDesktopName});
} catch (exc) { } catch (exc) {
@ -274,7 +271,7 @@
return; // don't continue trying to connect return; // don't continue trying to connect
} }
rfb.connect(host, port, password, path); rfb.connect(host, port, { password: password }, path);
})(); })();
</script> </script>
</head> </head>