Abstract RFB errors to avoid sending strings

The API allowed strings to be passed from the RFB module to the
application using the disconnect reason. This caused problems since
the application didn't have control over translations for these
strings.

Most of the information being passed using this string was very
technical and not helpful to the end user. One exception to this was
the security result information regarding for example authentication
failures. The protocol allows the VNC server to pass a string
directly to the user in the security result.

So the disconnect reason is replaced by a boolean saying if the
disconnection was clean or not. And for the security result information
from the server, a new event has been added.
This commit is contained in:
Samuel Mannehed 2017-11-11 01:48:47 +01:00
parent ee5cae9fee
commit d472f3f19e
7 changed files with 232 additions and 133 deletions

View File

@ -1023,6 +1023,7 @@ var UI = {
UI.rfb.addEventListener("connect", UI.connectFinished); UI.rfb.addEventListener("connect", UI.connectFinished);
UI.rfb.addEventListener("disconnect", UI.disconnectFinished); UI.rfb.addEventListener("disconnect", UI.disconnectFinished);
UI.rfb.addEventListener("credentialsrequired", UI.credentials); UI.rfb.addEventListener("credentialsrequired", UI.credentials);
UI.rfb.addEventListener("securityfailure", UI.securityFailed);
UI.rfb.addEventListener("capabilities", function () { UI.updatePowerButton(); UI.initialResize(); }); UI.rfb.addEventListener("capabilities", function () { UI.updatePowerButton(); UI.initialResize(); });
UI.rfb.addEventListener("clipboard", UI.clipboardReceive); UI.rfb.addEventListener("clipboard", UI.clipboardReceive);
UI.rfb.addEventListener("bell", UI.bell); UI.rfb.addEventListener("bell", UI.bell);
@ -1080,9 +1081,10 @@ var UI = {
// UI.disconnect() won't be used in those cases. // UI.disconnect() won't be used in those cases.
UI.connected = false; UI.connected = false;
if (typeof e.detail.reason !== 'undefined') { if (!e.detail.clean) {
UI.showStatus(e.detail.reason, 'error');
UI.updateVisualState('disconnected'); UI.updateVisualState('disconnected');
UI.showStatus(_("Something went wrong, connection is closed"),
'error');
} else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) { } else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) {
UI.updateVisualState('reconnecting'); UI.updateVisualState('reconnecting');
@ -1098,6 +1100,20 @@ var UI = {
UI.openConnectPanel(); UI.openConnectPanel();
}, },
securityFailed: function (e) {
let msg = "";
// On security failures we might get a string with a reason
// directly from the server. Note that we can't control if
// this string is translated or not.
if ('reason' in e.detail) {
msg = _("New connection has been rejected with reason: ") +
e.detail.reason;
} else {
msg = _("New connection has been rejected");
}
UI.showStatus(msg, 'error');
},
cancelReconnect: function() { cancelReconnect: function() {
if (UI.reconnect_callback !== null) { if (UI.reconnect_callback !== null) {
clearTimeout(UI.reconnect_callback); clearTimeout(UI.reconnect_callback);

View File

@ -11,7 +11,6 @@
*/ */
import * as Log from './util/logging.js'; import * as Log from './util/logging.js';
import _ from './util/localization.js';
import { decodeUTF8 } from './util/strings.js'; import { decodeUTF8 } from './util/strings.js';
import { browserSupportsCursorURIs, isTouchDevice } from './util/browsers.js'; import { browserSupportsCursorURIs, isTouchDevice } from './util/browsers.js';
import EventTargetMixin from './util/eventtarget.js'; import EventTargetMixin from './util/eventtarget.js';
@ -54,7 +53,7 @@ export default function RFB(target, url, options) {
this._rfb_connection_state = ''; this._rfb_connection_state = '';
this._rfb_init_state = ''; this._rfb_init_state = '';
this._rfb_auth_scheme = ''; this._rfb_auth_scheme = '';
this._rfb_disconnect_reason = ""; this._rfb_clean_disconnect = true;
// Server capabilities // Server capabilities
this._rfb_version = 0; this._rfb_version = 0;
@ -190,38 +189,40 @@ export default function RFB(target, url, options) {
this._rfb_init_state = 'ProtocolVersion'; this._rfb_init_state = 'ProtocolVersion';
Log.Debug("Starting VNC handshake"); Log.Debug("Starting VNC handshake");
} else { } else {
this._fail("Unexpected server connection"); this._fail("Unexpected server connection while " +
this._rfb_connection_state);
} }
}.bind(this)); }.bind(this));
this._sock.on('close', function (e) { this._sock.on('close', function (e) {
Log.Warn("WebSocket on-close event"); Log.Warn("WebSocket on-close event");
var msg = ""; var msg = "";
if (e.code) { if (e.code) {
msg = " (code: " + e.code; msg = "(code: " + e.code;
if (e.reason) { if (e.reason) {
msg += ", reason: " + e.reason; msg += ", reason: " + e.reason;
} }
msg += ")"; msg += ")";
} }
switch (this._rfb_connection_state) { switch (this._rfb_connection_state) {
case 'disconnecting':
this._updateConnectionState('disconnected');
break;
case 'connecting': case 'connecting':
this._fail('Failed to connect to server', msg); this._fail("Connection closed " + msg);
break; break;
case 'connected': case 'connected':
// Handle disconnects that were initiated server-side // Handle disconnects that were initiated server-side
this._updateConnectionState('disconnecting'); this._updateConnectionState('disconnecting');
this._updateConnectionState('disconnected'); this._updateConnectionState('disconnected');
break; break;
case 'disconnecting':
// Normal disconnection path
this._updateConnectionState('disconnected');
break;
case 'disconnected': case 'disconnected':
this._fail("Unexpected server disconnect", this._fail("Unexpected server disconnect " +
"Already disconnected: " + msg); "when already disconnected " + msg);
break; break;
default: default:
this._fail("Unexpected server disconnect", this._fail("Unexpected server disconnect before connecting " +
"Not in any state yet: " + msg); msg);
break; break;
} }
this._sock.off('close'); this._sock.off('close');
@ -384,9 +385,9 @@ RFB.prototype = {
this._sock.open(this._url, ['binary']); this._sock.open(this._url, ['binary']);
} catch (e) { } catch (e) {
if (e.name === 'SyntaxError') { if (e.name === 'SyntaxError') {
this._fail("Invalid host or port value given", e); this._fail("Invalid host or port (" + e + ")");
} else { } else {
this._fail("Error while connecting", e); this._fail("Error when opening socket (" + e + ")");
} }
} }
@ -539,19 +540,15 @@ RFB.prototype = {
this._disconnect(); this._disconnect();
this._disconnTimer = setTimeout(function () { this._disconnTimer = setTimeout(function () {
this._rfb_disconnect_reason = _("Disconnect timeout"); Log.Error("Disconnection timed out.");
this._updateConnectionState('disconnected'); this._updateConnectionState('disconnected');
}.bind(this), DISCONNECT_TIMEOUT * 1000); }.bind(this), DISCONNECT_TIMEOUT * 1000);
break; break;
case 'disconnected': case 'disconnected':
if (this._rfb_disconnect_reason !== "") { event = new CustomEvent(
event = new CustomEvent("disconnect", "disconnect", { detail:
{ detail: { reason: this._rfb_disconnect_reason } }); { clean: this._rfb_clean_disconnect } });
} else {
// No reason means clean disconnect
event = new CustomEvent("disconnect", { detail: {} });
}
this.dispatchEvent(event); this.dispatchEvent(event);
break; break;
} }
@ -559,29 +556,25 @@ RFB.prototype = {
/* Print errors and disconnect /* Print errors and disconnect
* *
* The optional parameter 'details' is used for information that * The parameter 'details' is used for information that
* should be logged but not sent to the user interface. * should be logged but not sent to the user interface.
*/ */
_fail: function (msg, details) { _fail: function (details) {
var fullmsg = msg;
if (typeof details !== 'undefined') {
fullmsg = msg + " (" + details + ")";
}
switch (this._rfb_connection_state) { switch (this._rfb_connection_state) {
case 'disconnecting': case 'disconnecting':
Log.Error("Failed when disconnecting: " + fullmsg); Log.Error("Failed when disconnecting: " + details);
break; break;
case 'connected': case 'connected':
Log.Error("Failed while connected: " + fullmsg); Log.Error("Failed while connected: " + details);
break; break;
case 'connecting': case 'connecting':
Log.Error("Failed when connecting: " + fullmsg); Log.Error("Failed when connecting: " + details);
break; break;
default: default:
Log.Error("RFB failure: " + fullmsg); Log.Error("RFB failure: " + details);
break; break;
} }
this._rfb_disconnect_reason = msg; //This is sent to the UI this._rfb_clean_disconnect = false; //This is sent to the UI
// Transition to disconnected without waiting for socket to close // Transition to disconnected without waiting for socket to close
this._updateConnectionState('disconnecting'); this._updateConnectionState('disconnecting');
@ -693,8 +686,7 @@ RFB.prototype = {
_negotiate_protocol_version: function () { _negotiate_protocol_version: function () {
if (this._sock.rQlen() < 12) { if (this._sock.rQlen() < 12) {
return this._fail("Error while negotiating with server", return this._fail("Received incomplete protocol version.");
"Incomplete protocol version");
} }
var sversion = this._sock.rQshiftStr(12).substr(4, 7); var sversion = this._sock.rQshiftStr(12).substr(4, 7);
@ -719,8 +711,7 @@ RFB.prototype = {
this._rfb_version = 3.8; this._rfb_version = 3.8;
break; break;
default: default:
return this._fail("Unsupported server", return this._fail("Invalid server version " + sversion);
"Invalid server version: " + sversion);
} }
if (is_repeater) { if (is_repeater) {
@ -762,10 +753,7 @@ RFB.prototype = {
if (this._sock.rQwait("security type", num_types, 1)) { return false; } if (this._sock.rQwait("security type", num_types, 1)) { return false; }
if (num_types === 0) { if (num_types === 0) {
var strlen = this._sock.rQshift32(); return this._handle_security_failure("no security types");
var reason = this._sock.rQshiftStr(strlen);
return this._fail("Error while negotiating with server",
"Security failure: " + reason);
} }
var types = this._sock.rQshiftBytes(num_types); var types = this._sock.rQshiftBytes(num_types);
@ -782,8 +770,7 @@ RFB.prototype = {
} else if (includes(2, types)) { } else if (includes(2, types)) {
this._rfb_auth_scheme = 2; // VNC Auth this._rfb_auth_scheme = 2; // VNC Auth
} else { } else {
return this._fail("Unsupported server", return this._fail("Unsupported security types (types: " + types + ")");
"Unsupported security types: " + types);
} }
this._sock.send([this._rfb_auth_scheme]); this._sock.send([this._rfb_auth_scheme]);
@ -799,6 +786,59 @@ RFB.prototype = {
return this._init_msg(); // jump to authentication return this._init_msg(); // jump to authentication
}, },
/*
* Get the security failure reason if sent from the server and
* send the 'securityfailure' event.
*
* - The optional parameter context can be used to add some extra
* context to the log output.
*
* - The optional parameter security_result_status can be used to
* add a custom status code to the event.
*/
_handle_security_failure: function (context, security_result_status) {
if (typeof context === 'undefined') {
context = "";
} else {
context = " on " + context;
}
if (typeof security_result_status === 'undefined') {
security_result_status = 1; // fail
}
if (this._sock.rQwait("reason length", 4)) {
return false;
}
let strlen = this._sock.rQshift32();
let reason = "";
if (strlen > 0) {
if (this._sock.rQwait("reason", strlen, 8)) { return false; }
reason = this._sock.rQshiftStr(strlen);
}
if (reason !== "") {
let event = new CustomEvent(
"securityfailure",
{ detail: { status: security_result_status, reason: reason } });
this.dispatchEvent(event);
return this._fail("Security negotiation failed" + context +
" (reason: " + reason + ")");
} else {
let event = new CustomEvent(
"securityfailure",
{ detail: { status: security_result_status } });
this.dispatchEvent(event);
return this._fail("Security negotiation failed" + context);
}
},
// authentication // authentication
_negotiate_xvp_auth: function () { _negotiate_xvp_auth: function () {
if (!this._rfb_credentials.username || if (!this._rfb_credentials.username ||
@ -854,15 +894,13 @@ RFB.prototype = {
if (serverSupportedTunnelTypes[0]) { if (serverSupportedTunnelTypes[0]) {
if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
return this._fail("Unsupported server", return this._fail("Client's tunnel type had the incorrect " +
"Client's tunnel type had the incorrect " +
"vendor or signature"); "vendor or signature");
} }
this._sock.send([0, 0, 0, 0]); // use NOTUNNEL this._sock.send([0, 0, 0, 0]); // use NOTUNNEL
return false; // wait until we receive the sub auth count to continue return false; // wait until we receive the sub auth count to continue
} else { } else {
return this._fail("Unsupported server", return this._fail("Server wanted tunnels, but doesn't support " +
"Server wanted tunnels, but doesn't support " +
"the notunnel type"); "the notunnel type");
} }
}, },
@ -916,24 +954,19 @@ RFB.prototype = {
this._rfb_auth_scheme = 2; this._rfb_auth_scheme = 2;
return this._init_msg(); return this._init_msg();
default: default:
return this._fail("Unsupported server", return this._fail("Unsupported tiny auth scheme " +
"Unsupported tiny auth scheme: " + "(scheme: " + authType + ")");
authType);
} }
} }
} }
return this._fail("Unsupported server", return this._fail("No supported sub-auth types!");
"No supported sub-auth types!");
}, },
_negotiate_authentication: function () { _negotiate_authentication: function () {
switch (this._rfb_auth_scheme) { switch (this._rfb_auth_scheme) {
case 0: // connection failed case 0: // connection failed
if (this._sock.rQwait("auth reason", 4)) { return false; } return this._handle_security_failure("authentication scheme");
var strlen = this._sock.rQshift32();
var reason = this._sock.rQshiftStr(strlen);
return this._fail("Authentication failure", reason);
case 1: // no auth case 1: // no auth
if (this._rfb_version >= 3.8) { if (this._rfb_version >= 3.8) {
@ -953,33 +986,30 @@ RFB.prototype = {
return this._negotiate_tight_auth(); return this._negotiate_tight_auth();
default: default:
return this._fail("Unsupported server", return this._fail("Unsupported auth scheme (scheme: " +
"Unsupported auth scheme: " + this._rfb_auth_scheme + ")");
this._rfb_auth_scheme);
} }
}, },
_handle_security_result: function () { _handle_security_result: function () {
if (this._sock.rQwait('VNC auth response ', 4)) { return false; } if (this._sock.rQwait('VNC auth response ', 4)) { return false; }
switch (this._sock.rQshift32()) {
case 0: // OK let status = this._sock.rQshift32();
this._rfb_init_state = 'ClientInitialisation';
Log.Debug('Authentication OK'); if (status === 0) { // OK
return this._init_msg(); this._rfb_init_state = 'ClientInitialisation';
case 1: // failed Log.Debug('Authentication OK');
if (this._rfb_version >= 3.8) { return this._init_msg();
var length = this._sock.rQshift32(); } else {
if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; } if (this._rfb_version >= 3.8) {
var reason = this._sock.rQshiftStr(length); return this._handle_security_failure("security result", status);
return this._fail("Authentication failure", reason); } else {
} else { let event = new CustomEvent("securityfailure",
return this._fail("Authentication failure"); { detail: { status: status } });
} this.dispatchEvent(event);
case 2:
return this._fail("Too many authentication attempts"); return this._fail("Security handshake failed");
default: }
return this._fail("Unsupported server",
"Unknown SecurityResult");
} }
}, },
@ -1158,15 +1188,15 @@ RFB.prototype = {
return this._negotiate_server_init(); return this._negotiate_server_init();
default: default:
return this._fail("Internal error", "Unknown init state: " + return this._fail("Unknown init state (state: " +
this._rfb_init_state); this._rfb_init_state + ")");
} }
}, },
_handle_set_colour_map_msg: function () { _handle_set_colour_map_msg: function () {
Log.Debug("SetColorMapEntries"); Log.Debug("SetColorMapEntries");
return this._fail("Protocol error", "Unexpected SetColorMapEntries message"); return this._fail("Unexpected SetColorMapEntries message");
}, },
_handle_server_cut_text: function () { _handle_server_cut_text: function () {
@ -1215,8 +1245,7 @@ RFB.prototype = {
*/ */
if (!(flags & (1<<31))) { if (!(flags & (1<<31))) {
return this._fail("Internal error", return this._fail("Unexpected fence response");
"Unexpected fence response");
} }
// Filter out unsupported flags // Filter out unsupported flags
@ -1247,8 +1276,7 @@ RFB.prototype = {
this._setCapability("power", true); this._setCapability("power", true);
break; break;
default: default:
this._fail("Unexpected server message", this._fail("Illegal server XVP message (msg: " + xvp_msg + ")");
"Illegal server XVP message " + xvp_msg);
break; break;
} }
@ -1306,7 +1334,7 @@ RFB.prototype = {
return this._handle_xvp_msg(); return this._handle_xvp_msg();
default: default:
this._fail("Unexpected server message", "Type:" + msg_type); this._fail("Unexpected server message (type " + msg_type + ")");
Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
return true; return true;
} }
@ -1361,9 +1389,8 @@ RFB.prototype = {
(hdr[10] << 8) + hdr[11], 10); (hdr[10] << 8) + hdr[11], 10);
if (!this._encHandlers[this._FBU.encoding]) { if (!this._encHandlers[this._FBU.encoding]) {
this._fail("Unexpected server message", this._fail("Unsupported encoding (encoding: " +
"Unsupported encoding " + this._FBU.encoding + ")");
this._FBU.encoding);
return false; return false;
} }
} }
@ -1857,8 +1884,8 @@ RFB.encodingHandlers = {
if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; } if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; }
var subencoding = rQ[rQi]; // Peek var subencoding = rQ[rQi]; // Peek
if (subencoding > 30) { // Raw if (subencoding > 30) { // Raw
this._fail("Unexpected server message", this._fail("Illegal hextile subencoding (subencoding: " +
"Illegal hextile subencoding: " + subencoding); subencoding + ")");
return false; return false;
} }
@ -2187,9 +2214,8 @@ RFB.encodingHandlers = {
else if (ctl === 0x0A) cmode = "png"; else if (ctl === 0x0A) cmode = "png";
else if (ctl & 0x04) cmode = "filter"; else if (ctl & 0x04) cmode = "filter";
else if (ctl < 0x04) cmode = "copy"; else if (ctl < 0x04) cmode = "copy";
else return this._fail("Unexpected server message", else return this._fail("Illegal tight compression received (ctl: " +
"Illegal tight compression received, " + ctl + ")");
"ctl: " + ctl);
switch (cmode) { switch (cmode) {
// fill use depth because TPIXELs drop the padding byte // fill use depth because TPIXELs drop the padding byte
@ -2249,9 +2275,8 @@ RFB.encodingHandlers = {
} else { } else {
// Filter 0, Copy could be valid here, but servers don't send it as an explicit filter // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter
// Filter 2, Gradient is valid but not use if jpeg is enabled // Filter 2, Gradient is valid but not use if jpeg is enabled
this._fail("Unexpected server message", this._fail("Unsupported tight subencoding received " +
"Unsupported tight subencoding received, " + "(filter: " + filterId + ")");
"filter: " + filterId);
} }
break; break;
case "copy": case "copy":

View File

@ -74,6 +74,10 @@ protocol stream.
- The `credentialsrequired` event is fired when more credentials must - The `credentialsrequired` event is fired when more credentials must
be given to continue. be given to continue.
[`securityfailure`](#securityfailure)
- The `securityfailure` event is fired when the security negotiation
with the server fails.
[`clipboard`](#clipboard) [`clipboard`](#clipboard)
- The `clipboard` event is fired when clipboard data is received from - The `clipboard` event is fired when clipboard data is received from
the server. the server.
@ -186,10 +190,10 @@ the `RFB` object is ready to recieve graphics updates and to send input.
#### disconnect #### disconnect
The `disconnect` event is fired when the connection has been The `disconnect` event is fired when the connection has been
terminated. The `detail` property is an `Object` the optionally terminated. The `detail` property is an `Object` that contains the
contains the property `reason`. `reason` is a `DOMString` specifying property `clean`. `clean` is a `boolean` indicating if the termination
the reason in the event of an unexpected termination. `reason` will be was clean or not. In the event of an unexpected termination or an error
omitted for a clean termination. `clean` will be set to false.
#### credentialsrequired #### credentialsrequired
@ -198,6 +202,26 @@ credentials than were specified to [`RFB()`](#rfb-1). The `detail`
property is an `Object` containing the property `types` which is an property is an `Object` containing the property `types` which is an
`Array` of `DOMString` listing the credentials that are required. `Array` of `DOMString` listing the credentials that are required.
#### securityfailure
The `securityfailure` event is fired when the handshaking process with
the server fails during the security negotiation step. The `detail`
property is an `Object` containing the following properties:
| Property | Type | Description
| -------- | ----------- | -----------
| `status` | `long` | The failure status code
| `reason` | `DOMString` | The **optional** reason for the failure
The property `status` corresponds to the
[SecurityResult](https://github.com/rfbproto/rfbproto/blob/master/rfbproto.rst#securityresult)
status code in cases of failure. A status of zero will not be sent in
this event since that indicates a successful security handshaking
process. The optional property `reason` is provided by the server and
thus the language of the string is not known. However most servers will
probably send English strings. The server can choose to not send a
reason and in these cases the `reason` property will be omitted.
#### clipboard #### clipboard
The `clipboard` event is fired when the server has sent clipboard data. The `clipboard` event is fired when the server has sent clipboard data.

View File

@ -115,13 +115,13 @@ IterationPlayer.prototype = {
this._nextIteration(); this._nextIteration();
}, },
_disconnected: function (rfb, reason, frame) { _disconnected: function (rfb, clean, frame) {
if (reason) { if (!clean) {
this._state = 'failed'; this._state = 'failed';
} }
var evt = new Event('rfbdisconnected'); var evt = new Event('rfbdisconnected');
evt.reason = reason; evt.clean = clean;
evt.frame = frame; evt.frame = frame;
this.onrfbdisconnected(evt); this.onrfbdisconnected(evt);

View File

@ -78,7 +78,8 @@ RecordingPlayer.prototype = {
// initialize a new RFB // initialize a new RFB
this._rfb = new RFB(document.getElementById('VNC_canvas'), 'wss://test'); this._rfb = new RFB(document.getElementById('VNC_canvas'), 'wss://test');
this._rfb.viewOnly = true; this._rfb.viewOnly = true;
this._rfb.ondisconnected = this._handleDisconnect.bind(this); this._rfb.addEventListener("disconnect",
this._handleDisconnect.bind(this));
this._enablePlaybackMode(); this._enablePlaybackMode();
// reset the frame index and timer // reset the frame index and timer
@ -186,8 +187,8 @@ RecordingPlayer.prototype = {
} }
}, },
_handleDisconnect(rfb, reason) { _handleDisconnect(rfb, clean) {
this._running = false; this._running = false;
this._disconnected(rfb, reason, this._frame_index); this._disconnected(rfb, clean, this._frame_index);
} }
}; };

View File

@ -377,26 +377,21 @@ describe('Remote Frame Buffer Protocol Client', function() {
expect(client._rfb_connection_state).to.equal('disconnected'); expect(client._rfb_connection_state).to.equal('disconnected');
}); });
it('should set disconnect_reason', function () { it('should set clean_disconnect variable', function () {
client._rfb_clean_disconnect = true;
client._rfb_connection_state = 'connected'; client._rfb_connection_state = 'connected';
client._fail('a reason'); client._fail();
expect(client._rfb_disconnect_reason).to.equal('a reason'); expect(client._rfb_clean_disconnect).to.be.false;
}); });
it('should not include details in disconnect_reason', function () { it('should result in disconnect event with clean set to false', function () {
client._rfb_connection_state = 'connected';
client._fail('a reason', 'details');
expect(client._rfb_disconnect_reason).to.equal('a reason');
});
it('should result in disconnect callback with message when reason given', function () {
client._rfb_connection_state = 'connected'; client._rfb_connection_state = 'connected';
var spy = sinon.spy(); var spy = sinon.spy();
client.addEventListener("disconnect", spy); client.addEventListener("disconnect", spy);
client._fail('a reason'); client._fail();
this.clock.tick(2000); this.clock.tick(2000);
expect(spy).to.have.been.calledOnce; expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.reason).to.equal('a reason'); expect(spy.args[0][0].detail.clean).to.be.false;
}); });
}); });
@ -471,17 +466,16 @@ describe('Remote Frame Buffer Protocol Client', function() {
var client; var client;
beforeEach(function () { client = make_rfb(); }); beforeEach(function () { client = make_rfb(); });
it('should call the disconnect callback if the state is "disconnected"', function () { it('should result in a disconnect event if state becomes "disconnected"', function () {
var spy = sinon.spy(); var spy = sinon.spy();
client.addEventListener("disconnect", spy); client.addEventListener("disconnect", spy);
client._rfb_connection_state = 'disconnecting'; client._rfb_connection_state = 'disconnecting';
client._rfb_disconnect_reason = "error";
client._updateConnectionState('disconnected'); client._updateConnectionState('disconnected');
expect(spy).to.have.been.calledOnce; expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.reason).to.equal("error"); expect(spy.args[0][0].detail.clean).to.be.true;
}); });
it('should not call the disconnect callback if the state is not "disconnected"', function () { it('should not result in a disconnect event if the state is not "disconnected"', function () {
var spy = sinon.spy(); var spy = sinon.spy();
client.addEventListener("disconnect", spy); client.addEventListener("disconnect", spy);
client._sock._websocket.close = function () {}; // explicitly don't call onclose client._sock._websocket.close = function () {}; // explicitly don't call onclose
@ -489,7 +483,7 @@ describe('Remote Frame Buffer Protocol Client', function() {
expect(spy).to.not.have.been.called; expect(spy).to.not.have.been.called;
}); });
it('should call the disconnect callback without msg when no reason given', function () { it('should result in a disconnect event without msg when no reason given', function () {
var spy = sinon.spy(); var spy = sinon.spy();
client.addEventListener("disconnect", spy); client.addEventListener("disconnect", spy);
client._rfb_connection_state = 'disconnecting'; client._rfb_connection_state = 'disconnecting';
@ -653,7 +647,7 @@ describe('Remote Frame Buffer Protocol Client', function() {
expect(client._fail).to.have.been.calledOnce; expect(client._fail).to.have.been.calledOnce;
expect(client._fail).to.have.been.calledWith( expect(client._fail).to.have.been.calledWith(
'Error while negotiating with server','Security failure: whoops'); 'Security negotiation failed on no security types (reason: whoops)');
}); });
it('should transition to the Authentication state and continue on successful negotiation', function () { it('should transition to the Authentication state and continue on successful negotiation', function () {
@ -688,7 +682,7 @@ describe('Remote Frame Buffer Protocol Client', function() {
sinon.spy(client, '_fail'); sinon.spy(client, '_fail');
client._sock._websocket._receive_data(new Uint8Array(data)); client._sock._websocket._receive_data(new Uint8Array(data));
expect(client._fail).to.have.been.calledWith( expect(client._fail).to.have.been.calledWith(
'Authentication failure', 'Whoopsies'); 'Security negotiation failed on authentication scheme (reason: Whoopsies)');
}); });
it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () { it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () {
@ -909,14 +903,53 @@ describe('Remote Frame Buffer Protocol Client', function() {
var failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; var failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115];
client._sock._websocket._receive_data(new Uint8Array(failure_data)); client._sock._websocket._receive_data(new Uint8Array(failure_data));
expect(client._fail).to.have.been.calledWith( expect(client._fail).to.have.been.calledWith(
'Authentication failure', 'whoops'); 'Security negotiation failed on security result (reason: whoops)');
}); });
it('should fail on an error code of 1 with a standard message for version < 3.8', function () { it('should fail on an error code of 1 with a standard message for version < 3.8', function () {
sinon.spy(client, '_fail'); sinon.spy(client, '_fail');
client._rfb_version = 3.7; client._rfb_version = 3.7;
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1])); client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1]));
expect(client._fail).to.have.been.calledWith('Authentication failure'); expect(client._fail).to.have.been.calledWith(
'Security handshake failed');
});
it('should result in securityfailure event when receiving a non zero status', function () {
var spy = sinon.spy();
client.addEventListener("securityfailure", spy);
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 2]));
expect(spy).to.have.been.calledOnce;
expect(spy.args[0][0].detail.status).to.equal(2);
});
it('should include reason when provided in securityfailure event', function () {
client._rfb_version = 3.8;
var spy = sinon.spy();
client.addEventListener("securityfailure", spy);
var failure_data = [0, 0, 0, 1, 0, 0, 0, 12, 115, 117, 99, 104,
32, 102, 97, 105, 108, 117, 114, 101];
client._sock._websocket._receive_data(new Uint8Array(failure_data));
expect(spy.args[0][0].detail.status).to.equal(1);
expect(spy.args[0][0].detail.reason).to.equal('such failure');
});
it('should not include reason when length is zero in securityfailure event', function () {
client._rfb_version = 3.9;
var spy = sinon.spy();
client.addEventListener("securityfailure", spy);
var failure_data = [0, 0, 0, 1, 0, 0, 0, 0];
client._sock._websocket._receive_data(new Uint8Array(failure_data));
expect(spy.args[0][0].detail.status).to.equal(1);
expect('reason' in spy.args[0][0].detail).to.be.false;
});
it('should not include reason in securityfailure event for version < 3.8', function () {
client._rfb_version = 3.6;
var spy = sinon.spy();
client.addEventListener("securityfailure", spy);
client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 2]));
expect(spy.args[0][0].detail.status).to.equal(2);
expect('reason' in spy.args[0][0].detail).to.be.false;
}); });
}); });

View File

@ -162,10 +162,10 @@
function disconnected(e) { function disconnected(e) {
document.getElementById('sendCtrlAltDelButton').disabled = true; document.getElementById('sendCtrlAltDelButton').disabled = true;
updatePowerButtons(); updatePowerButtons();
if (typeof(e.detail.reason) !== 'undefined') { if (e.detail.clean) {
status(e.detail.reason, "error");
} else {
status("Disconnected", "normal"); status("Disconnected", "normal");
} else {
status("Something went wrong, connection is closed", "error");
} }
} }