From a1e59b9d3aa9b7fda49b9eb5e081700f46580b97 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 10 Dec 2017 17:46:11 -0800 Subject: [PATCH 1/5] GUACAMOLE-567: Add HTTP and WebSocket translation functions to Guacamole.Status.Code. --- .../src/main/webapp/modules/Status.js | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) 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; + +}; From ca98d07b4abe8f37a0d8eb51bd4a4043cbc2c3d1 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 10 Dec 2017 16:56:19 -0800 Subject: [PATCH 2/5] GUACAMOLE-567: Rely on HTTP or WebSocket status code to determine error if Guacamole-specific reason is missing. Default to server unreachable. --- .../src/main/webapp/modules/Tunnel.js | 84 +++++++------------ 1 file changed, 31 insertions(+), 53 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js index c8f8502b5..63e27c52b 100644 --- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js +++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js @@ -382,10 +382,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)); } @@ -808,13 +821,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 +1163,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 +1225,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(); }; From e6f36659954653271495ee2c12cd80469a2ee63a Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 10 Dec 2017 20:22:22 -0800 Subject: [PATCH 3/5] GUACAMOLE-567: Add UNSTABLE tunnel status. Mark tunnel as UNSTABLE if no data has been received in a reasonable amount of time, but the tunnel is technically still open. --- .../src/main/webapp/modules/Tunnel.js | 75 +++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js index 63e27c52b..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; @@ -682,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. @@ -733,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); + } /** @@ -754,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; From 1ed22401bbc2b1b076f49190530dbcf61bd4809f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 10 Dec 2017 20:28:35 -0800 Subject: [PATCH 4/5] GUACAMOLE-567: Warn user when tunnel enters "UNSTABLE" state. --- .../client/controllers/clientController.js | 12 +++++++ .../app/client/styles/connection-warning.css | 36 +++++++++++++++++++ .../webapp/app/client/templates/client.html | 5 +++ .../webapp/app/client/types/ManagedClient.js | 12 +++++++ .../app/client/types/ManagedClientState.js | 11 +++++- .../src/main/webapp/translations/en.json | 1 + 6 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 guacamole/src/main/webapp/app/client/styles/connection-warning.css 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..eec3e0745 --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/connection-warning.css @@ -0,0 +1,36 @@ +/* + * 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; + top: 0.25em; + z-index: 20; + + max-width: 100%; + max-height: 3in; + + border: 1px solid rgba(0,0,0,0.5); + box-shadow: 1px 1px 2px rgba(0,0,0,0.25); + background: #FFE; + padding: 0.5em; + font-size: .8em; + +} 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 @@ + +
+ {{'CLIENT.TEXT_CLIENT_STATUS_UNSTABLE' | translate}} +
+