diff --git a/app/ui.js b/app/ui.js index 0ee0c00b..cadf44b1 100644 --- a/app/ui.js +++ b/app/ui.js @@ -1023,6 +1023,7 @@ var UI = { UI.rfb.addEventListener("connect", UI.connectFinished); UI.rfb.addEventListener("disconnect", UI.disconnectFinished); UI.rfb.addEventListener("credentialsrequired", UI.credentials); + UI.rfb.addEventListener("securityfailure", UI.securityFailed); UI.rfb.addEventListener("capabilities", function () { UI.updatePowerButton(); UI.initialResize(); }); UI.rfb.addEventListener("clipboard", UI.clipboardReceive); UI.rfb.addEventListener("bell", UI.bell); @@ -1080,9 +1081,10 @@ var UI = { // UI.disconnect() won't be used in those cases. UI.connected = false; - if (typeof e.detail.reason !== 'undefined') { - UI.showStatus(e.detail.reason, 'error'); + if (!e.detail.clean) { UI.updateVisualState('disconnected'); + UI.showStatus(_("Something went wrong, connection is closed"), + 'error'); } else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) { UI.updateVisualState('reconnecting'); @@ -1098,6 +1100,20 @@ var UI = { 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() { if (UI.reconnect_callback !== null) { clearTimeout(UI.reconnect_callback); diff --git a/core/rfb.js b/core/rfb.js index dced6b16..aa58c00f 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -11,7 +11,6 @@ */ import * as Log from './util/logging.js'; -import _ from './util/localization.js'; import { decodeUTF8 } from './util/strings.js'; import { browserSupportsCursorURIs, isTouchDevice } from './util/browsers.js'; import EventTargetMixin from './util/eventtarget.js'; @@ -54,7 +53,7 @@ export default function RFB(target, url, options) { this._rfb_connection_state = ''; this._rfb_init_state = ''; this._rfb_auth_scheme = ''; - this._rfb_disconnect_reason = ""; + this._rfb_clean_disconnect = true; // Server capabilities this._rfb_version = 0; @@ -190,38 +189,40 @@ export default function RFB(target, url, options) { this._rfb_init_state = 'ProtocolVersion'; Log.Debug("Starting VNC handshake"); } else { - this._fail("Unexpected server connection"); + this._fail("Unexpected server connection while " + + this._rfb_connection_state); } }.bind(this)); this._sock.on('close', function (e) { Log.Warn("WebSocket on-close event"); var msg = ""; if (e.code) { - msg = " (code: " + e.code; + msg = "(code: " + e.code; if (e.reason) { msg += ", reason: " + e.reason; } msg += ")"; } switch (this._rfb_connection_state) { - case 'disconnecting': - this._updateConnectionState('disconnected'); - break; case 'connecting': - this._fail('Failed to connect to server', msg); + this._fail("Connection closed " + msg); break; case 'connected': // Handle disconnects that were initiated server-side this._updateConnectionState('disconnecting'); this._updateConnectionState('disconnected'); break; + case 'disconnecting': + // Normal disconnection path + this._updateConnectionState('disconnected'); + break; case 'disconnected': - this._fail("Unexpected server disconnect", - "Already disconnected: " + msg); + this._fail("Unexpected server disconnect " + + "when already disconnected " + msg); break; default: - this._fail("Unexpected server disconnect", - "Not in any state yet: " + msg); + this._fail("Unexpected server disconnect before connecting " + + msg); break; } this._sock.off('close'); @@ -384,9 +385,9 @@ RFB.prototype = { this._sock.open(this._url, ['binary']); } catch (e) { if (e.name === 'SyntaxError') { - this._fail("Invalid host or port value given", e); + this._fail("Invalid host or port (" + e + ")"); } else { - this._fail("Error while connecting", e); + this._fail("Error when opening socket (" + e + ")"); } } @@ -539,19 +540,15 @@ RFB.prototype = { this._disconnect(); this._disconnTimer = setTimeout(function () { - this._rfb_disconnect_reason = _("Disconnect timeout"); + Log.Error("Disconnection timed out."); this._updateConnectionState('disconnected'); }.bind(this), DISCONNECT_TIMEOUT * 1000); break; case 'disconnected': - if (this._rfb_disconnect_reason !== "") { - event = new CustomEvent("disconnect", - { detail: { reason: this._rfb_disconnect_reason } }); - } else { - // No reason means clean disconnect - event = new CustomEvent("disconnect", { detail: {} }); - } + event = new CustomEvent( + "disconnect", { detail: + { clean: this._rfb_clean_disconnect } }); this.dispatchEvent(event); break; } @@ -559,29 +556,25 @@ RFB.prototype = { /* 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. */ - _fail: function (msg, details) { - var fullmsg = msg; - if (typeof details !== 'undefined') { - fullmsg = msg + " (" + details + ")"; - } + _fail: function (details) { switch (this._rfb_connection_state) { case 'disconnecting': - Log.Error("Failed when disconnecting: " + fullmsg); + Log.Error("Failed when disconnecting: " + details); break; case 'connected': - Log.Error("Failed while connected: " + fullmsg); + Log.Error("Failed while connected: " + details); break; case 'connecting': - Log.Error("Failed when connecting: " + fullmsg); + Log.Error("Failed when connecting: " + details); break; default: - Log.Error("RFB failure: " + fullmsg); + Log.Error("RFB failure: " + details); 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 this._updateConnectionState('disconnecting'); @@ -693,8 +686,7 @@ RFB.prototype = { _negotiate_protocol_version: function () { if (this._sock.rQlen() < 12) { - return this._fail("Error while negotiating with server", - "Incomplete protocol version"); + return this._fail("Received incomplete protocol version."); } var sversion = this._sock.rQshiftStr(12).substr(4, 7); @@ -719,8 +711,7 @@ RFB.prototype = { this._rfb_version = 3.8; break; default: - return this._fail("Unsupported server", - "Invalid server version: " + sversion); + return this._fail("Invalid server version " + sversion); } if (is_repeater) { @@ -762,10 +753,7 @@ RFB.prototype = { if (this._sock.rQwait("security type", num_types, 1)) { return false; } if (num_types === 0) { - var strlen = this._sock.rQshift32(); - var reason = this._sock.rQshiftStr(strlen); - return this._fail("Error while negotiating with server", - "Security failure: " + reason); + return this._handle_security_failure("no security types"); } var types = this._sock.rQshiftBytes(num_types); @@ -782,8 +770,7 @@ RFB.prototype = { } else if (includes(2, types)) { this._rfb_auth_scheme = 2; // VNC Auth } else { - return this._fail("Unsupported server", - "Unsupported security types: " + types); + return this._fail("Unsupported security types (types: " + types + ")"); } this._sock.send([this._rfb_auth_scheme]); @@ -799,6 +786,59 @@ RFB.prototype = { 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 _negotiate_xvp_auth: function () { if (!this._rfb_credentials.username || @@ -854,15 +894,13 @@ RFB.prototype = { if (serverSupportedTunnelTypes[0]) { if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { - return this._fail("Unsupported server", - "Client's tunnel type had the incorrect " + + return this._fail("Client's tunnel type had the incorrect " + "vendor or signature"); } this._sock.send([0, 0, 0, 0]); // use NOTUNNEL return false; // wait until we receive the sub auth count to continue } else { - return this._fail("Unsupported server", - "Server wanted tunnels, but doesn't support " + + return this._fail("Server wanted tunnels, but doesn't support " + "the notunnel type"); } }, @@ -916,24 +954,19 @@ RFB.prototype = { this._rfb_auth_scheme = 2; return this._init_msg(); default: - return this._fail("Unsupported server", - "Unsupported tiny auth scheme: " + - authType); + return this._fail("Unsupported tiny auth scheme " + + "(scheme: " + authType + ")"); } } } - return this._fail("Unsupported server", - "No supported sub-auth types!"); + return this._fail("No supported sub-auth types!"); }, _negotiate_authentication: function () { switch (this._rfb_auth_scheme) { case 0: // connection failed - if (this._sock.rQwait("auth reason", 4)) { return false; } - var strlen = this._sock.rQshift32(); - var reason = this._sock.rQshiftStr(strlen); - return this._fail("Authentication failure", reason); + return this._handle_security_failure("authentication scheme"); case 1: // no auth if (this._rfb_version >= 3.8) { @@ -953,33 +986,30 @@ RFB.prototype = { return this._negotiate_tight_auth(); default: - return this._fail("Unsupported server", - "Unsupported auth scheme: " + - this._rfb_auth_scheme); + return this._fail("Unsupported auth scheme (scheme: " + + this._rfb_auth_scheme + ")"); } }, _handle_security_result: function () { if (this._sock.rQwait('VNC auth response ', 4)) { return false; } - switch (this._sock.rQshift32()) { - case 0: // OK - this._rfb_init_state = 'ClientInitialisation'; - Log.Debug('Authentication OK'); - return this._init_msg(); - case 1: // failed - if (this._rfb_version >= 3.8) { - var length = this._sock.rQshift32(); - if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; } - var reason = this._sock.rQshiftStr(length); - return this._fail("Authentication failure", reason); - } else { - return this._fail("Authentication failure"); - } - case 2: - return this._fail("Too many authentication attempts"); - default: - return this._fail("Unsupported server", - "Unknown SecurityResult"); + + let status = this._sock.rQshift32(); + + if (status === 0) { // OK + this._rfb_init_state = 'ClientInitialisation'; + Log.Debug('Authentication OK'); + return this._init_msg(); + } else { + if (this._rfb_version >= 3.8) { + return this._handle_security_failure("security result", status); + } else { + let event = new CustomEvent("securityfailure", + { detail: { status: status } }); + this.dispatchEvent(event); + + return this._fail("Security handshake failed"); + } } }, @@ -1158,15 +1188,15 @@ RFB.prototype = { return this._negotiate_server_init(); default: - return this._fail("Internal error", "Unknown init state: " + - this._rfb_init_state); + return this._fail("Unknown init state (state: " + + this._rfb_init_state + ")"); } }, _handle_set_colour_map_msg: function () { Log.Debug("SetColorMapEntries"); - return this._fail("Protocol error", "Unexpected SetColorMapEntries message"); + return this._fail("Unexpected SetColorMapEntries message"); }, _handle_server_cut_text: function () { @@ -1215,8 +1245,7 @@ RFB.prototype = { */ if (!(flags & (1<<31))) { - return this._fail("Internal error", - "Unexpected fence response"); + return this._fail("Unexpected fence response"); } // Filter out unsupported flags @@ -1247,8 +1276,7 @@ RFB.prototype = { this._setCapability("power", true); break; default: - this._fail("Unexpected server message", - "Illegal server XVP message " + xvp_msg); + this._fail("Illegal server XVP message (msg: " + xvp_msg + ")"); break; } @@ -1306,7 +1334,7 @@ RFB.prototype = { return this._handle_xvp_msg(); 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)); return true; } @@ -1361,9 +1389,8 @@ RFB.prototype = { (hdr[10] << 8) + hdr[11], 10); if (!this._encHandlers[this._FBU.encoding]) { - this._fail("Unexpected server message", - "Unsupported encoding " + - this._FBU.encoding); + this._fail("Unsupported encoding (encoding: " + + this._FBU.encoding + ")"); return false; } } @@ -1857,8 +1884,8 @@ RFB.encodingHandlers = { if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; } var subencoding = rQ[rQi]; // Peek if (subencoding > 30) { // Raw - this._fail("Unexpected server message", - "Illegal hextile subencoding: " + subencoding); + this._fail("Illegal hextile subencoding (subencoding: " + + subencoding + ")"); return false; } @@ -2187,9 +2214,8 @@ RFB.encodingHandlers = { else if (ctl === 0x0A) cmode = "png"; else if (ctl & 0x04) cmode = "filter"; else if (ctl < 0x04) cmode = "copy"; - else return this._fail("Unexpected server message", - "Illegal tight compression received, " + - "ctl: " + ctl); + else return this._fail("Illegal tight compression received (ctl: " + + ctl + ")"); switch (cmode) { // fill use depth because TPIXELs drop the padding byte @@ -2249,9 +2275,8 @@ RFB.encodingHandlers = { } else { // 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 - this._fail("Unexpected server message", - "Unsupported tight subencoding received, " + - "filter: " + filterId); + this._fail("Unsupported tight subencoding received " + + "(filter: " + filterId + ")"); } break; case "copy": diff --git a/docs/API.md b/docs/API.md index e194e969..4d4f4d51 100644 --- a/docs/API.md +++ b/docs/API.md @@ -74,6 +74,10 @@ protocol stream. - The `credentialsrequired` event is fired when more credentials must be given to continue. +[`securityfailure`](#securityfailure) + - The `securityfailure` event is fired when the security negotiation + with the server fails. + [`clipboard`](#clipboard) - The `clipboard` event is fired when clipboard data is received from the server. @@ -186,10 +190,10 @@ the `RFB` object is ready to recieve graphics updates and to send input. #### disconnect The `disconnect` event is fired when the connection has been -terminated. The `detail` property is an `Object` the optionally -contains the property `reason`. `reason` is a `DOMString` specifying -the reason in the event of an unexpected termination. `reason` will be -omitted for a clean termination. +terminated. The `detail` property is an `Object` that contains the +property `clean`. `clean` is a `boolean` indicating if the termination +was clean or not. In the event of an unexpected termination or an error +`clean` will be set to false. #### 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 `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 The `clipboard` event is fired when the server has sent clipboard data. diff --git a/tests/playback-ui.js b/tests/playback-ui.js index 03b08fe1..01ad241e 100644 --- a/tests/playback-ui.js +++ b/tests/playback-ui.js @@ -115,13 +115,13 @@ IterationPlayer.prototype = { this._nextIteration(); }, - _disconnected: function (rfb, reason, frame) { - if (reason) { + _disconnected: function (rfb, clean, frame) { + if (!clean) { this._state = 'failed'; } var evt = new Event('rfbdisconnected'); - evt.reason = reason; + evt.clean = clean; evt.frame = frame; this.onrfbdisconnected(evt); diff --git a/tests/playback.js b/tests/playback.js index 9199eba0..c769e88f 100644 --- a/tests/playback.js +++ b/tests/playback.js @@ -78,7 +78,8 @@ RecordingPlayer.prototype = { // initialize a new RFB this._rfb = new RFB(document.getElementById('VNC_canvas'), 'wss://test'); this._rfb.viewOnly = true; - this._rfb.ondisconnected = this._handleDisconnect.bind(this); + this._rfb.addEventListener("disconnect", + this._handleDisconnect.bind(this)); this._enablePlaybackMode(); // reset the frame index and timer @@ -186,8 +187,8 @@ RecordingPlayer.prototype = { } }, - _handleDisconnect(rfb, reason) { + _handleDisconnect(rfb, clean) { this._running = false; - this._disconnected(rfb, reason, this._frame_index); + this._disconnected(rfb, clean, this._frame_index); } }; diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 2ef61240..81ee1dd6 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -377,26 +377,21 @@ describe('Remote Frame Buffer Protocol Client', function() { 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._fail('a reason'); - expect(client._rfb_disconnect_reason).to.equal('a reason'); + client._fail(); + expect(client._rfb_clean_disconnect).to.be.false; }); - it('should not include details in disconnect_reason', 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 () { + it('should result in disconnect event with clean set to false', function () { client._rfb_connection_state = 'connected'; var spy = sinon.spy(); client.addEventListener("disconnect", spy); - client._fail('a reason'); + client._fail(); this.clock.tick(2000); 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; 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(); client.addEventListener("disconnect", spy); client._rfb_connection_state = 'disconnecting'; - client._rfb_disconnect_reason = "error"; client._updateConnectionState('disconnected'); 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(); client.addEventListener("disconnect", spy); 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; }); - 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(); client.addEventListener("disconnect", spy); 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.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 () { @@ -688,7 +682,7 @@ describe('Remote Frame Buffer Protocol Client', function() { sinon.spy(client, '_fail'); client._sock._websocket._receive_data(new Uint8Array(data)); 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 () { @@ -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]; client._sock._websocket._receive_data(new Uint8Array(failure_data)); 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 () { sinon.spy(client, '_fail'); client._rfb_version = 3.7; 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; }); }); diff --git a/vnc_lite.html b/vnc_lite.html index b904b4a5..762da013 100644 --- a/vnc_lite.html +++ b/vnc_lite.html @@ -162,10 +162,10 @@ function disconnected(e) { document.getElementById('sendCtrlAltDelButton').disabled = true; updatePowerButtons(); - if (typeof(e.detail.reason) !== 'undefined') { - status(e.detail.reason, "error"); - } else { + if (e.detail.clean) { status("Disconnected", "normal"); + } else { + status("Something went wrong, connection is closed", "error"); } }