diff --git a/guacamole-common-js/src/main/webapp/modules/Status.js b/guacamole-common-js/src/main/webapp/modules/Status.js
index ceadaa68c..3acfd63c8 100644
--- a/guacamole-common-js/src/main/webapp/modules/Status.js
+++ b/guacamole-common-js/src/main/webapp/modules/Status.js
@@ -232,3 +232,87 @@ Guacamole.Status.Code = {
"CLIENT_TOO_MANY": 0x031D
};
+
+/**
+ * Returns the Guacamole protocol status code which most closely
+ * represents the given HTTP status code.
+ *
+ * @param {Number} status
+ * The HTTP status code to translate into a Guacamole protocol status
+ * code.
+ *
+ * @returns {Number}
+ * The Guacamole protocol status code which most closely represents the
+ * given HTTP status code.
+ */
+Guacamole.Status.Code.fromHTTPCode = function fromHTTPCode(status) {
+
+ // Translate status codes with known equivalents
+ switch (status) {
+
+ // HTTP 400 - Bad request
+ case 400:
+ return Guacamole.Status.Code.CLIENT_BAD_REQUEST;
+
+ // HTTP 403 - Forbidden
+ case 403:
+ return Guacamole.Status.Code.CLIENT_FORBIDDEN;
+
+ // HTTP 404 - Resource not found
+ case 404:
+ return Guacamole.Status.Code.RESOURCE_NOT_FOUND;
+
+ // HTTP 429 - Too many requests
+ case 429:
+ return Guacamole.Status.Code.CLIENT_TOO_MANY;
+
+ // HTTP 503 - Server unavailable
+ case 503:
+ return Guacamole.Status.Code.SERVER_BUSY;
+
+ }
+
+ // Default all other codes to generic internal error
+ return Guacamole.Status.Code.SERVER_ERROR;
+
+};
+
+/**
+ * Returns the Guacamole protocol status code which most closely
+ * represents the given WebSocket status code.
+ *
+ * @param {Number} code
+ * The WebSocket status code to translate into a Guacamole protocol
+ * status code.
+ *
+ * @returns {Number}
+ * The Guacamole protocol status code which most closely represents the
+ * given WebSocket status code.
+ */
+Guacamole.Status.Code.fromWebSocketCode = function fromWebSocketCode(code) {
+
+ // Translate status codes with known equivalents
+ switch (code) {
+
+ // Successful disconnect (no error)
+ case 1000: // Normal Closure
+ return Guacamole.Status.Code.SUCCESS;
+
+ // Codes which indicate the server is not reachable
+ case 1006: // Abnormal Closure (also signalled by JavaScript when the connection cannot be opened in the first place)
+ case 1015: // TLS Handshake
+ return Guacamole.Status.Code.UPSTREAM_NOT_FOUND;
+
+ // Codes which indicate the server is reachable but busy/unavailable
+ case 1001: // Going Away
+ case 1012: // Service Restart
+ case 1013: // Try Again Later
+ case 1014: // Bad Gateway
+ return Guacamole.Status.Code.UPSTREAM_UNAVAILABLE;
+
+ }
+
+ // Default all other codes to generic internal error
+ return Guacamole.Status.Code.SERVER_ERROR;
+
+};
diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js
index c8f8502b5..52bd20ac2 100644
--- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js
+++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js
@@ -84,11 +84,22 @@ Guacamole.Tunnel = function() {
* The maximum amount of time to wait for data to be received, in
* milliseconds. If data is not received within this amount of time,
* the tunnel is closed with an error. The default value is 15000.
- *
+ *
* @type {Number}
*/
this.receiveTimeout = 15000;
+ /**
+ * The amount of time to wait for data to be received before considering
+ * the connection to be unstable, in milliseconds. If data is not received
+ * within this amount of time, the tunnel status is updated to warn that
+ * the connection appears unresponsive and may close. The default value is
+ * 1500.
+ *
+ * @type {Number}
+ */
+ this.unstableThreshold = 1500;
+
/**
* The UUID uniquely identifying this tunnel. If not yet known, this will
* be null.
@@ -165,7 +176,15 @@ Guacamole.Tunnel.State = {
*
* @type {Number}
*/
- "CLOSED": 2
+ "CLOSED": 2,
+
+ /**
+ * The connection is open, but communication through the tunnel appears to
+ * be disrupted, and the connection may close as a result.
+ *
+ * @type {Number}
+ */
+ "UNSTABLE" : 3
};
@@ -219,6 +238,14 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
*/
var receive_timeout = null;
+ /**
+ * The current connection stability timeout ID, if any.
+ *
+ * @private
+ * @type {Number}
+ */
+ var unstableTimeout = null;
+
/**
* Additional headers to be sent in tunnel requests. This dictionary can be
* populated with key/value header pairs to pass information such as authentication
@@ -253,14 +280,24 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
*/
function reset_timeout() {
- // Get rid of old timeout (if any)
+ // Get rid of old timeouts (if any)
window.clearTimeout(receive_timeout);
+ window.clearTimeout(unstableTimeout);
- // Set new timeout
+ // Clear unstable status
+ if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
+ tunnel.setState(Guacamole.Tunnel.State.OPEN);
+
+ // Set new timeout for tracking overall connection timeout
receive_timeout = window.setTimeout(function () {
close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
}, tunnel.receiveTimeout);
+ // Set new timeout for tracking suspected connection instability
+ unstableTimeout = window.setTimeout(function() {
+ tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
+ }, tunnel.unstableThreshold);
+
}
/**
@@ -274,6 +311,10 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
*/
function close_tunnel(status) {
+ // Get rid of old timeouts (if any)
+ window.clearTimeout(receive_timeout);
+ window.clearTimeout(unstableTimeout);
+
// Ignore if already closed
if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
return;
@@ -382,10 +423,23 @@ Guacamole.HTTPTunnel = function(tunnelURL, crossDomain, extraTunnelHeaders) {
function handleHTTPTunnelError(xmlhttprequest) {
+ // Pull status code directly from headers provided by Guacamole
var code = parseInt(xmlhttprequest.getResponseHeader("Guacamole-Status-Code"));
- var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message");
+ if (code) {
+ var message = xmlhttprequest.getResponseHeader("Guacamole-Error-Message");
+ close_tunnel(new Guacamole.Status(code, message));
+ }
- close_tunnel(new Guacamole.Status(code, message));
+ // Failing that, derive a Guacamole status code from the HTTP status
+ // code provided by the browser
+ else if (xmlhttprequest.status)
+ close_tunnel(new Guacamole.Status(
+ Guacamole.Status.Code.fromHTTPCode(xmlhttprequest.status),
+ xmlhttprequest.statusText));
+
+ // Otherwise, assume server is unreachable
+ else
+ close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
}
@@ -669,6 +723,14 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
*/
var receive_timeout = null;
+ /**
+ * The current connection stability timeout ID, if any.
+ *
+ * @private
+ * @type {Number}
+ */
+ var unstableTimeout = null;
+
/**
* The WebSocket protocol corresponding to the protocol used for the current
* location.
@@ -720,14 +782,24 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
*/
function reset_timeout() {
- // Get rid of old timeout (if any)
+ // Get rid of old timeouts (if any)
window.clearTimeout(receive_timeout);
+ window.clearTimeout(unstableTimeout);
- // Set new timeout
+ // Clear unstable status
+ if (tunnel.state === Guacamole.Tunnel.State.UNSTABLE)
+ tunnel.setState(Guacamole.Tunnel.State.OPEN);
+
+ // Set new timeout for tracking overall connection timeout
receive_timeout = window.setTimeout(function () {
close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_TIMEOUT, "Server timeout."));
}, tunnel.receiveTimeout);
+ // Set new timeout for tracking suspected connection instability
+ unstableTimeout = window.setTimeout(function() {
+ tunnel.setState(Guacamole.Tunnel.State.UNSTABLE);
+ }, tunnel.unstableThreshold);
+
}
/**
@@ -741,6 +813,10 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
*/
function close_tunnel(status) {
+ // Get rid of old timeouts (if any)
+ window.clearTimeout(receive_timeout);
+ window.clearTimeout(unstableTimeout);
+
// Ignore if already closed
if (tunnel.state === Guacamole.Tunnel.State.CLOSED)
return;
@@ -808,13 +884,22 @@ Guacamole.WebSocketTunnel = function(tunnelURL) {
};
socket.onclose = function(event) {
- close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason));
+
+ // Pull status code directly from closure reason provided by Guacamole
+ if (event.reason)
+ close_tunnel(new Guacamole.Status(parseInt(event.reason), event.reason));
+
+ // Failing that, derive a Guacamole status code from the WebSocket
+ // status code provided by the browser
+ else if (event.code)
+ close_tunnel(new Guacamole.Status(Guacamole.Status.Code.fromWebSocketCode(event.code)));
+
+ // Otherwise, assume server is unreachable
+ else
+ close_tunnel(new Guacamole.Status(Guacamole.Status.Code.UPSTREAM_NOT_FOUND));
+
};
- socket.onerror = function(event) {
- close_tunnel(new Guacamole.Status(Guacamole.Status.Code.SERVER_ERROR, event.data));
- };
-
socket.onmessage = function(event) {
reset_timeout();
@@ -1141,51 +1226,6 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
}
}
- /**
- * Returns the Guacamole protocol status code which most closely
- * represents the given HTTP status code.
- *
- * @private
- * @param {Number} httpStatus
- * The HTTP status code to translate into a Guacamole protocol status
- * code.
- *
- * @returns {Number}
- * The Guacamole protocol status code which most closely represents the
- * given HTTP status code.
- */
- var getGuacamoleStatusCode = function getGuacamoleStatusCode(httpStatus) {
-
- // Translate status codes with known equivalents
- switch (httpStatus) {
-
- // HTTP 400 - Bad request
- case 400:
- return Guacamole.Status.Code.CLIENT_BAD_REQUEST;
-
- // HTTP 403 - Forbidden
- case 403:
- return Guacamole.Status.Code.CLIENT_FORBIDDEN;
-
- // HTTP 404 - Resource not found
- case 404:
- return Guacamole.Status.Code.RESOURCE_NOT_FOUND;
-
- // HTTP 429 - Too many requests
- case 429:
- return Guacamole.Status.Code.CLIENT_TOO_MANY;
-
- // HTTP 503 - Server unavailable
- case 503:
- return Guacamole.Status.Code.SERVER_BUSY;
-
- }
-
- // Default all other codes to generic internal error
- return Guacamole.Status.Code.SERVER_ERROR;
-
- };
-
this.sendMessage = function sendMessage(elements) {
// Do nothing
};
@@ -1248,7 +1288,8 @@ Guacamole.StaticHTTPTunnel = function StaticHTTPTunnel(url, crossDomain, extraTu
// Fail if file could not be downloaded via HTTP
if (tunnel.onerror)
- tunnel.onerror(new Guacamole.Status(getGuacamoleStatusCode(xhr.status), xhr.statusText));
+ tunnel.onerror(new Guacamole.Status(
+ Guacamole.Status.Code.fromHTTPCode(xhr.status), xhr.statusText));
tunnel.disconnect();
};
diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js
index af1d72609..ffbe3c539 100644
--- a/guacamole/src/main/webapp/app/client/controllers/clientController.js
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -626,6 +626,18 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams
};
+ /**
+ * Returns whether the current connection has been flagged as unstable due
+ * to an apparent network disruption.
+ *
+ * @returns {Boolean}
+ * true if the current connection has been flagged as unstable, false
+ * otherwise.
+ */
+ $scope.isConnectionUnstable = function isConnectionUnstable() {
+ return $scope.client && $scope.client.clientState.connectionState === ManagedClientState.ConnectionState.UNSTABLE;
+ };
+
// Show status dialog when connection status changes
$scope.$watch('client.clientState.connectionState', function clientStateChanged(connectionState) {
diff --git a/guacamole/src/main/webapp/app/client/styles/connection-warning.css b/guacamole/src/main/webapp/app/client/styles/connection-warning.css
new file mode 100644
index 000000000..87af0a882
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/styles/connection-warning.css
@@ -0,0 +1,56 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+#connection-warning {
+
+ position: absolute;
+ right: 0.25em;
+ bottom: 0.25em;
+ z-index: 20;
+
+ width: 3in;
+ max-width: 100%;
+ min-height: 1em;
+
+ border-left: 2em solid #FA0;
+ box-shadow: 1px 1px 2px rgba(0,0,0,0.25);
+ background: #FFE;
+ padding: 0.5em 0.75em;
+ font-size: .8em;
+
+}
+
+#connection-warning::before {
+
+ content: ' ';
+ display: block;
+ position: absolute;
+ left: -2em;
+ top: 0;
+
+ width: 1.25em;
+ height: 100%;
+ margin: 0 0.375em;
+
+ background: url('images/warning.png');
+ background-size: contain;
+ background-position: center;
+ background-repeat: no-repeat;
+
+}
diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html
index 054cbcf67..ad85f234e 100644
--- a/guacamole/src/main/webapp/app/client/templates/client.html
+++ b/guacamole/src/main/webapp/app/client/templates/client.html
@@ -36,6 +36,11 @@