diff --git a/app/styles/base.css b/app/styles/base.css
index 03f6583e..b90bcb26 100644
--- a/app/styles/base.css
+++ b/app/styles/base.css
@@ -172,6 +172,43 @@ input[type=button]:active, select:active {
pointer-events: auto;
}
+/* ----------------------------------------
+ * Fallback error
+ * ----------------------------------------
+ */
+
+#noVNC_fallback_error {
+ position: fixed;
+ z-index: 3;
+ left: 50%;
+ transform: translate(-50%, -50px);
+ transition: 0.5s ease-in-out;
+
+ visibility: hidden;
+ opacity: 0;
+
+ top: 60px;
+ padding: 15px;
+ width: auto;
+
+ text-align: center;
+ font-weight: bold;
+ word-wrap: break-word;
+ color: #fff;
+
+ border-radius: 10px;
+ box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.5);
+ background: rgba(200,55,55,0.8);
+}
+#noVNC_fallback_error.noVNC_open {
+ transform: translate(-50%, 0);
+ visibility: visible;
+ opacity: 1;
+}
+#noVNC_fallback_errormsg {
+ font-weight: normal;
+}
+
/* ----------------------------------------
* Control Bar
* ----------------------------------------
diff --git a/app/ui.js b/app/ui.js
index 1e55652b..ceb72092 100644
--- a/app/ui.js
+++ b/app/ui.js
@@ -25,6 +25,21 @@ var UI;
(function () {
"use strict";
+ // Fallback for all uncought errors
+ window.addEventListener('error', function(msg, url, line) {
+ try {
+ document.getElementById('noVNC_fallback_error')
+ .classList.add("noVNC_open");
+ document.getElementById('noVNC_fallback_errormsg').innerHTML =
+ url + ' (' + line + ')
' + msg;
+ } catch (exc) {
+ document.write("noVNC encountered an error.");
+ }
+ // Don't return true since this would prevent the error
+ // from being printed to the browser console.
+ return false;
+ });
+
/* [begin skip-as-module] */
// Load supporting scripts
WebUtil.load_scripts(
diff --git a/core/rfb.js b/core/rfb.js
index 421bf540..d10d6662 100644
--- a/core/rfb.js
+++ b/core/rfb.js
@@ -213,7 +213,7 @@
this._rfb_init_state = 'ProtocolVersion';
Util.Debug("Starting VNC handshake");
} else {
- this._fail("Got unexpected WebSocket connection");
+ this._fail("Unexpected server connection");
}
}.bind(this));
this._sock.on('close', function (e) {
@@ -231,13 +231,20 @@
this._updateConnectionState('disconnected');
break;
case 'connecting':
- this._fail('Failed to connect to server' + msg);
+ this._fail('Failed to connect to server', msg);
+ break;
+ case 'connected':
+ // Handle disconnects that were initiated server-side
+ this._updateConnectionState('disconnecting');
+ this._updateConnectionState('disconnected');
break;
case 'disconnected':
- Util.Error("Received onclose while disconnected" + msg);
+ this._fail("Unexpected server disconnect",
+ "Already disconnected: " + msg);
break;
default:
- this._fail("Server disconnected" + msg);
+ this._fail("Unexpected server disconnect",
+ "Not in any state yet: " + msg);
break;
}
this._sock.off('close');
@@ -343,7 +350,7 @@
requestDesktopSize: function (width, height) {
if (this._rfb_connection_state !== 'connected' ||
this._view_only) {
- return;
+ return false;
}
if (this._supportsSetDesktopSize) {
@@ -361,6 +368,7 @@
_connect: function () {
Util.Debug(">> RFB.connect");
+ this._init_vars();
var uri;
if (typeof UsingSocketIO !== 'undefined') {
@@ -372,11 +380,28 @@
uri += '://' + this._rfb_host + ':' + this._rfb_port + '/' + this._rfb_path;
Util.Info("connecting to " + uri);
- this._sock.open(uri, this._wsProtocols);
+ try {
+ // WebSocket.onopen transitions to the RFB init states
+ this._sock.open(uri, this._wsProtocols);
+ } catch (e) {
+ if (e.name === 'SyntaxError') {
+ this._fail("Invalid host or port value given", e);
+ } else {
+ this._fail("Error while connecting", e);
+ }
+ }
Util.Debug("<< RFB.connect");
},
+ _disconnect: function () {
+ Util.Debug(">> RFB.disconnect");
+ this._cleanup();
+ this._sock.close();
+ this._print_stats();
+ Util.Debug("<< RFB.disconnect");
+ },
+
_init_vars: function () {
// reset state
this._FBU.rects = 0;
@@ -450,19 +475,7 @@
return;
}
- this._rfb_connection_state = state;
-
- var smsg = "New state '" + state + "', was '" + oldstate + "'.";
- Util.Debug(smsg);
-
- if (this._disconnTimer && state !== 'disconnecting') {
- Util.Debug("Clearing disconnect timer");
- clearTimeout(this._disconnTimer);
- this._disconnTimer = null;
- this._sock.off('close'); // make sure we don't get a double event
- }
-
- this._onUpdateState(this, state, oldstate);
+ // Ensure proper transitions before doing anything
switch (state) {
case 'connected':
if (oldstate !== 'connecting') {
@@ -478,7 +491,50 @@
"previous connection state: " + oldstate);
return;
}
+ break;
+ case 'connecting':
+ if (oldstate !== '') {
+ Util.Error("Bad transition to connecting state, " +
+ "previous connection state: " + oldstate);
+ return;
+ }
+ break;
+
+ case 'disconnecting':
+ if (oldstate !== 'connected' && oldstate !== 'connecting') {
+ Util.Error("Bad transition to disconnecting state, " +
+ "previous connection state: " + oldstate);
+ return;
+ }
+ break;
+
+ default:
+ Util.Error("Unknown connection state: " + state);
+ return;
+ }
+
+ // State change actions
+
+ this._rfb_connection_state = state;
+ this._onUpdateState(this, state, oldstate);
+
+ var smsg = "New state '" + state + "', was '" + oldstate + "'.";
+ Util.Debug(smsg);
+
+ if (this._disconnTimer && state !== 'disconnecting') {
+ Util.Debug("Clearing disconnect timer");
+ clearTimeout(this._disconnTimer);
+ this._disconnTimer = null;
+
+ // make sure we don't get a double event
+ this._sock.off('close');
+ }
+
+ switch (state) {
+ case 'disconnected':
+ // Call onDisconnected callback after onUpdateState since
+ // we don't know if the UI only displays the latest message
if (this._rfb_disconnect_reason !== "") {
this._onDisconnected(this, this._rfb_disconnect_reason);
} else {
@@ -488,47 +544,50 @@
break;
case 'connecting':
- this._init_vars();
-
- // WebSocket.onopen transitions to the RFB init states
this._connect();
break;
case 'disconnecting':
- this._cleanup();
- this._sock.close(); // transitions to 'disconnected'
+ this._disconnect();
this._disconnTimer = setTimeout(function () {
this._rfb_disconnect_reason = "Disconnect timeout";
this._updateConnectionState('disconnected');
}.bind(this), this._disconnectTimeout * 1000);
-
- this._print_stats();
break;
-
- default:
- Util.Error("Unknown connection state: " + state);
- return;
}
},
- _fail: function (msg) {
+ /* Print errors and disconnect
+ *
+ * The optional 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 + ")";
+ }
switch (this._rfb_connection_state) {
case 'disconnecting':
- Util.Error("Error while disconnecting: " + msg);
+ Util.Error("Failed when disconnecting: " + fullmsg);
break;
case 'connected':
- Util.Error("Error while connected: " + msg);
+ Util.Error("Failed while connected: " + fullmsg);
break;
case 'connecting':
- Util.Error("Error while connecting: " + msg);
+ Util.Error("Failed when connecting: " + fullmsg);
break;
default:
- Util.Error("RFB error: " + msg);
+ Util.Error("RFB failure: " + fullmsg);
break;
}
- this._rfb_disconnect_reason = msg;
+ this._rfb_disconnect_reason = msg; //This is sent to the UI
+
+ // Transition to disconnected without waiting for socket to close
this._updateConnectionState('disconnecting');
+ this._updateConnectionState('disconnected');
+
return false;
},
@@ -669,7 +728,8 @@
_negotiate_protocol_version: function () {
if (this._sock.rQlen() < 12) {
- return this._fail("Incomplete protocol version");
+ return this._fail("Error while negotiating with server",
+ "Incomplete protocol version");
}
var sversion = this._sock.rQshiftStr(12).substr(4, 7);
@@ -694,7 +754,8 @@
this._rfb_version = 3.8;
break;
default:
- return this._fail("Invalid server version " + sversion);
+ return this._fail("Unsupported server",
+ "Invalid server version: " + sversion);
}
if (is_repeater) {
@@ -727,7 +788,8 @@
if (num_types === 0) {
var strlen = this._sock.rQshift32();
var reason = this._sock.rQshiftStr(strlen);
- return this._fail("Security failure: " + reason);
+ return this._fail("Error while negotiating with server",
+ "Security failure: " + reason);
}
this._rfb_auth_scheme = 0;
@@ -749,7 +811,8 @@
}
if (this._rfb_auth_scheme === 0) {
- return this._fail("Unsupported security types: " + types);
+ return this._fail("Unsupported server",
+ "Unsupported security types: " + types);
}
this._sock.send([this._rfb_auth_scheme]);
@@ -819,12 +882,16 @@
if (serverSupportedTunnelTypes[0]) {
if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor ||
serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) {
- return this._fail("Client's tunnel type had the incorrect vendor or signature");
+ return this._fail("Unsupported server",
+ "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("Server wanted tunnels, but doesn't support the notunnel type");
+ return this._fail("Unsupported server",
+ "Server wanted tunnels, but doesn't support " +
+ "the notunnel type");
}
},
@@ -877,12 +944,15 @@
this._rfb_auth_scheme = 2;
return this._init_msg();
default:
- return this._fail("Unsupported tiny auth scheme: " + authType);
+ return this._fail("Unsupported server",
+ "Unsupported tiny auth scheme: " +
+ authType);
}
}
}
- return this._fail("No supported sub-auth types!");
+ return this._fail("Unsupported server",
+ "No supported sub-auth types!");
},
_negotiate_authentication: function () {
@@ -891,7 +961,7 @@
if (this._sock.rQwait("auth reason", 4)) { return false; }
var strlen = this._sock.rQshift32();
var reason = this._sock.rQshiftStr(strlen);
- return this._fail("Auth failure: " + reason);
+ return this._fail("Authentication failure", reason);
case 1: // no auth
if (this._rfb_version >= 3.8) {
@@ -911,7 +981,9 @@
return this._negotiate_tight_auth();
default:
- return this._fail("Unsupported auth scheme: " + this._rfb_auth_scheme);
+ return this._fail("Unsupported server",
+ "Unsupported auth scheme: " +
+ this._rfb_auth_scheme);
}
},
@@ -927,15 +999,16 @@
var length = this._sock.rQshift32();
if (this._sock.rQwait("SecurityResult reason", length, 8)) { return false; }
var reason = this._sock.rQshiftStr(length);
- return this._fail(reason);
+ return this._fail("Authentication failure", reason);
} else {
return this._fail("Authentication failure");
}
return false;
case 2:
- return this._fail("Too many auth attempts");
+ return this._fail("Too many authentication attempts");
default:
- return this._fail("Unknown SecurityResult");
+ return this._fail("Unsupported server",
+ "Unknown SecurityResult");
}
},
@@ -1083,7 +1156,7 @@
return this._negotiate_server_init();
default:
- return this._fail("Unknown init state: " +
+ return this._fail("Internal error", "Unknown init state: " +
this._rfb_init_state);
}
},
@@ -1148,7 +1221,8 @@
*/
if (!(flags & (1<<31))) {
- return this._fail("Unexpected fence response");
+ return this._fail("Internal error",
+ "Unexpected fence response");
}
// Filter out unsupported flags
@@ -1180,7 +1254,8 @@
this._onXvpInit(this._rfb_xvp_ver);
break;
default:
- this._fail("Disconnected: illegal server XVP message " + xvp_msg);
+ this._fail("Unexpected server message",
+ "Illegal server XVP message " + xvp_msg);
break;
}
@@ -1239,7 +1314,7 @@
return this._handle_xvp_msg();
default:
- this._fail("Disconnected: illegal server message type " + msg_type);
+ this._fail("Unexpected server message", "Type:" + msg_type);
Util.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30));
return true;
}
@@ -1300,7 +1375,8 @@
'encodingName': this._encNames[this._FBU.encoding]});
if (!this._encNames[this._FBU.encoding]) {
- this._fail("Disconnected: unsupported encoding " +
+ this._fail("Unexpected server message",
+ "Unsupported encoding " +
this._FBU.encoding);
return false;
}
@@ -1807,7 +1883,8 @@
if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; }
var subencoding = rQ[rQi]; // Peek
if (subencoding > 30) { // Raw
- this._fail("Disconnected: illegal hextile subencoding " + subencoding);
+ this._fail("Unexpected server message",
+ "Illegal hextile subencoding: " + subencoding);
return false;
}
@@ -1939,7 +2016,9 @@
display_tight: function (isTightPNG) {
if (this._fb_depth === 1) {
- this._fail("Tight protocol handler only implements true color mode");
+ this._fail("Internal error",
+ "Tight protocol handler only implements " +
+ "true color mode");
}
this._FBU.bytes = 1; // compression-control byte
@@ -2169,10 +2248,13 @@
else if (ctl === 0x0A) cmode = "png";
else if (ctl & 0x04) cmode = "filter";
else if (ctl < 0x04) cmode = "copy";
- else return this._fail("Illegal tight compression received, ctl: " + ctl);
+ else return this._fail("Unexpected server message",
+ "Illegal tight compression received, " +
+ "ctl: " + ctl);
if (isTightPNG && (cmode === "filter" || cmode === "copy")) {
- return this._fail("filter/copy received in tightPNG mode");
+ return this._fail("Unexpected server message",
+ "filter/copy received in tightPNG mode");
}
switch (cmode) {
@@ -2233,7 +2315,9 @@
} 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("Unsupported tight subencoding received, filter: " + filterId);
+ this._fail("Unexpected server message",
+ "Unsupported tight subencoding received, " +
+ "filter: " + filterId);
}
break;
case "copy":
diff --git a/tests/test.rfb.js b/tests/test.rfb.js
index b8bc9d22..ae51bffc 100644
--- a/tests/test.rfb.js
+++ b/tests/test.rfb.js
@@ -330,7 +330,7 @@ describe('Remote Frame Buffer Protocol Client', function() {
it('should clear the disconnect timer if the state is not "disconnecting"', function () {
var spy = sinon.spy();
client._disconnTimer = setTimeout(spy, 50);
- client._updateConnectionState('connected');
+ client._updateConnectionState('connecting');
this.clock.tick(51);
expect(spy).to.not.have.been.called;
expect(client._disconnTimer).to.be.null;
@@ -338,27 +338,37 @@ describe('Remote Frame Buffer Protocol Client', function() {
it('should call the updateState callback', function () {
client.set_onUpdateState(sinon.spy());
- client._updateConnectionState('a specific state');
+ client._updateConnectionState('connecting');
var spy = client.get_onUpdateState();
expect(spy).to.have.been.calledOnce;
- expect(spy.args[0][1]).to.equal('a specific state');
+ expect(spy.args[0][1]).to.equal('connecting');
});
it('should set the rfb_connection_state', function () {
- client._updateConnectionState('a specific state');
- expect(client._rfb_connection_state).to.equal('a specific state');
+ client._rfb_connection_state = 'disconnecting';
+ client._updateConnectionState('disconnected');
+ expect(client._rfb_connection_state).to.equal('disconnected');
});
it('should not change the state when we are disconnected', function () {
client._rfb_connection_state = 'disconnected';
- client._updateConnectionState('a specific state');
- expect(client._rfb_connection_state).to.not.equal('a specific state');
+ client._updateConnectionState('connecting');
+ expect(client._rfb_connection_state).to.not.equal('connecting');
});
it('should ignore state changes to the same state', function () {
client.set_onUpdateState(sinon.spy());
- client._rfb_connection_state = 'a specific state';
- client._updateConnectionState('a specific state');
+ client._rfb_connection_state = 'connecting';
+ client._updateConnectionState('connecting');
+ var spy = client.get_onUpdateState();
+ expect(spy).to.not.have.been.called;
+ });
+
+ it('should ignore illegal state changes', function () {
+ client.set_onUpdateState(sinon.spy());
+ client._rfb_connection_state = 'connected';
+ client._updateConnectionState('disconnected');
+ expect(client._rfb_connection_state).to.not.equal('disconnected');
var spy = client.get_onUpdateState();
expect(spy).to.not.have.been.called;
});
@@ -391,11 +401,19 @@ describe('Remote Frame Buffer Protocol Client', function() {
});
it('should set disconnect_reason', function () {
+ client._rfb_connection_state = 'connected';
client._fail('a reason');
expect(client._rfb_disconnect_reason).to.equal('a reason');
});
+ 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 () {
+ client._rfb_connection_state = 'connected';
client.set_onDisconnected(sinon.spy());
client._fail('a reason');
var spy = client.get_onDisconnected();
@@ -542,7 +560,7 @@ describe('Remote Frame Buffer Protocol Client', function() {
it('should call the updateState callback before the disconnect callback', function () {
client.set_onDisconnected(sinon.spy());
client.set_onUpdateState(sinon.spy());
- client._rfb_connection_state = 'other state';
+ client._rfb_connection_state = 'disconnecting';
client._updateConnectionState('disconnected');
var updateStateSpy = client.get_onUpdateState();
var disconnectSpy = client.get_onDisconnected();
@@ -717,7 +735,8 @@ describe('Remote Frame Buffer Protocol Client', function() {
client._sock._websocket._receive_data(failure_data);
expect(client._fail).to.have.been.calledOnce;
- expect(client._fail).to.have.been.calledWith('Security failure: whoops');
+ expect(client._fail).to.have.been.calledWith(
+ 'Error while negotiating with server','Security failure: whoops');
});
it('should transition to the Authentication state and continue on successful negotiation', function () {
@@ -756,7 +775,8 @@ 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('Auth failure: Whoopsies');
+ expect(client._fail).to.have.been.calledWith(
+ 'Authentication failure', 'Whoopsies');
});
it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () {
@@ -988,7 +1008,8 @@ describe('Remote Frame Buffer Protocol Client', function() {
sinon.spy(client, '_fail');
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('whoops');
+ expect(client._fail).to.have.been.calledWith(
+ 'Authentication failure', 'whoops');
});
it('should fail on an error code of 1 with a standard message for version < 3.8', function () {
@@ -2112,10 +2133,18 @@ describe('Remote Frame Buffer Protocol Client', function() {
expect(client._rfb_connection_state).to.equal('disconnected');
});
- it('should transition to failed if we get a close event from any non-"disconnection" state', function () {
+ it('should fail if we get a close event while connecting', function () {
sinon.spy(client, "_fail");
client.connect('host', 8675);
- client._rfb_connection_state = 'connected';
+ client._rfb_connection_state = 'connecting';
+ client._sock._websocket.close();
+ expect(client._fail).to.have.been.calledOnce;
+ });
+
+ it('should fail if we get a close event while disconnected', function () {
+ sinon.spy(client, "_fail");
+ client.connect('host', 8675);
+ client._rfb_connection_state = 'disconnected';
client._sock._websocket.close();
expect(client._fail).to.have.been.calledOnce;
});
diff --git a/vnc.html b/vnc.html
index 01828b44..ef880d20 100644
--- a/vnc.html
+++ b/vnc.html
@@ -65,6 +65,12 @@