From 688ff5310cf59e6b5c85ee692dcdb4a36fe04311 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 1 Nov 2020 20:21:09 -0800 Subject: [PATCH 1/3] GUACAMOLE-221: Add client support for the "required" instruction. --- .../src/main/webapp/modules/Client.js | 16 ++++++++++++++++ .../protocol/GuacamoleProtocolCapability.java | 11 ++++++++++- .../protocol/GuacamoleProtocolVersion.java | 9 ++++++++- 3 files changed, 34 insertions(+), 2 deletions(-) diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js index ed8085d29..18d8412a6 100644 --- a/guacamole-common-js/src/main/webapp/modules/Client.js +++ b/guacamole-common-js/src/main/webapp/modules/Client.js @@ -713,6 +713,18 @@ Guacamole.Client = function(tunnel) { * @param {String} name The name of the pipe. */ this.onpipe = null; + + /** + * Fired when a "required" instruction is received. A required instruction + * indicates that additional parameters are required for the connection to + * continue, such as user credentials. + * + * @event + * @param {String[]} parameters + * The names of the connection parameters that are required to be + * provided for the connection to continue. + */ + this.onrequired = null; /** * Fired whenever a sync instruction is received from the server, indicating @@ -1337,6 +1349,10 @@ Guacamole.Client = function(tunnel) { display.rect(layer, x, y, w, h); }, + + "required": function required(parameters) { + if (guac_client.onrequired) guac_client.onrequired(parameters); + }, "reset": function(parameters) { diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolCapability.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolCapability.java index 79f73f813..5cad8a280 100644 --- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolCapability.java +++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolCapability.java @@ -40,7 +40,16 @@ public enum GuacamoleProtocolCapability { * {@link GuacamoleProtocolVersion#VERSION_1_1_0}. */ PROTOCOL_VERSION_DETECTION(GuacamoleProtocolVersion.VERSION_1_1_0), - + + /** + * Support for the "required" instruction. The "required" instruction + * allows the server to explicitly request connection parameters from the + * client without which the connection cannot continue, such as user + * credentials. Support for this instruction was introduced in + * {@link GuacamoleProtocolVersion#VERSION_1_3_0}. + */ + REQUIRED_INSTRUCTION(GuacamoleProtocolVersion.VERSION_1_3_0), + /** * Support for the "timezone" handshake instruction. The "timezone" * instruction allows the client to request that the server forward their diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolVersion.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolVersion.java index c1d50baca..3ceda3b1a 100644 --- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolVersion.java +++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolVersion.java @@ -46,11 +46,18 @@ public class GuacamoleProtocolVersion { */ public static final GuacamoleProtocolVersion VERSION_1_1_0 = new GuacamoleProtocolVersion(1, 1, 0); + /** + * Protocol version 1.3.0, which introduces the "required" instruction + * allowing the server to explicitly request connection parameters from the + * client. + */ + public static final GuacamoleProtocolVersion VERSION_1_3_0 = new GuacamoleProtocolVersion(1, 3, 0); + /** * The most recent version of the Guacamole protocol at the time this * version of GuacamoleProtocolVersion was built. */ - public static final GuacamoleProtocolVersion LATEST = VERSION_1_1_0; + public static final GuacamoleProtocolVersion LATEST = VERSION_1_3_0; /** * A regular expression that matches the VERSION_X_Y_Z pattern, where From 718145ce242476b08db61de1d59727931755439f Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 1 Nov 2020 20:22:22 -0800 Subject: [PATCH 2/3] GUACAMOLE-221: Extend the guacNotification service to support generic parameter prompting. --- .../app/notification/styles/notification.css | 43 ++++++++++++++++++- .../templates/guacNotification.html | 12 +++++- .../app/notification/types/Notification.js | 35 +++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) diff --git a/guacamole/src/main/webapp/app/notification/styles/notification.css b/guacamole/src/main/webapp/app/notification/styles/notification.css index ee20e132f..3d5575341 100644 --- a/guacamole/src/main/webapp/app/notification/styles/notification.css +++ b/guacamole/src/main/webapp/app/notification/styles/notification.css @@ -100,4 +100,45 @@ .notification .progress .text { position: relative; -} \ No newline at end of file +} + +.notification .parameters { + width: 100%; +} + +.notification .parameters .fields { + display: table; + width: 100%; +} + +.notification .parameters .fields .labeled-field { + display: table-row; +} + +.notification .parameters .fields .field-header, +.notification .parameters .fields .form-field { + text-align: left; + display: table-cell; + padding: .125em; + vertical-align: top; +} + +.notification .parameters .fields .field-header { + padding-right: 1em; +} + +.notification .parameters .fields .field-header { + width: 0; +} + +.notification .parameters .fields .form-field { + width: 100%; +} + +.notification .parameters input[type=text], +.notification .parameters input[type=email], +.notification .parameters input[type=number], +.notification .parameters input[type=password], +.notification .parameters textarea { + max-width: 100%; +} diff --git a/guacamole/src/main/webapp/app/notification/templates/guacNotification.html b/guacamole/src/main/webapp/app/notification/templates/guacNotification.html index 6c1cc0086..b002bb0f7 100644 --- a/guacamole/src/main/webapp/app/notification/templates/guacNotification.html +++ b/guacamole/src/main/webapp/app/notification/templates/guacNotification.html @@ -1,4 +1,5 @@ -
+
@@ -12,6 +13,15 @@ translate="{{notification.text.key}}" translate-values="{{notification.text.variables}}">

+ +
+ +
+
+ */ + this.formModel = template.model; + + /** + * The function to invoke when the form is submitted, if form fields + * are present within the notification. + * + * @type Function + */ + this.formSubmitCallback = template.formSubmitCallback; + /** * An array of all actions available to the user in response to this * notification. From f3101688e253c7eac136821900c706e40bec1bab Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Sun, 1 Nov 2020 20:22:52 -0800 Subject: [PATCH 3/3] GUACAMOLE-221: Prompt user to provide additional parameters when "required" instruction is received. --- .../client/controllers/clientController.js | 109 +++++++++++++++++- .../webapp/app/client/styles/notification.css | 23 ++++ .../webapp/app/client/types/ManagedClient.js | 43 +++++++ .../app/index/controllers/indexController.js | 10 +- .../src/main/webapp/translations/en.json | 2 + 5 files changed, 181 insertions(+), 6 deletions(-) create mode 100644 guacamole/src/main/webapp/app/client/styles/notification.css diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js index 326d868a5..451adeeb9 100644 --- a/guacamole/src/main/webapp/app/client/controllers/clientController.js +++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js @@ -732,8 +732,14 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams return $scope.client && $scope.client.clientState.tunnelUnstable; }; - // Show status dialog when connection status changes - $scope.$watch('client.clientState.connectionState', function clientStateChanged(connectionState) { + /** + * Notifies the user that the connection state has changed. + * + * @param {String} connectionState + * The current connection state, as defined by + * ManagedClientState.ConnectionState. + */ + var notifyConnectionState = function notifyConnectionState(connectionState) { // Hide any existing status guacNotification.showStatus(false); @@ -835,6 +841,105 @@ angular.module('client').controller('clientController', ['$scope', '$routeParams else guacNotification.showStatus(false); + }; + + /** + * Prompts the user to enter additional connection parameters. If the + * protocol and associated parameters of the underlying connection are not + * yet known, this function has no effect and should be re-invoked once + * the parameters are known. + * + * @param {Object.} requiredParameters + * The set of all parameters requested by the server via "required" + * instructions, where each object key is the name of a requested + * parameter and each value is the current value entered by the user. + */ + var notifyParametersRequired = function notifyParametersRequired(requiredParameters) { + + /** + * Action which submits the current set of parameter values, requesting + * that the connection continue. + */ + var SUBMIT_PARAMETERS = { + name : "CLIENT.ACTION_CONTINUE", + className : "button", + callback : function submitParameters() { + if ($scope.client) { + var params = $scope.client.requiredParameters; + $scope.client.requiredParameters = null; + ManagedClient.sendArguments($scope.client, params); + } + } + }; + + /** + * Action which cancels submission of additional parameters and + * disconnects from the current connection. + */ + var CANCEL_PARAMETER_SUBMISSION = { + name : "CLIENT.ACTION_CANCEL", + className : "button", + callback : function cancelSubmission() { + $scope.client.requiredParameters = null; + $scope.disconnect(); + } + }; + + // Attempt to prompt for parameters only if the parameters that apply + // to the underlying connection are known + if (!$scope.client.protocol || !$scope.client.forms) + return; + + // Hide any existing status + guacNotification.showStatus(false); + + // Prompt for parameters + guacNotification.showStatus({ + formNamespace : Protocol.getNamespace($scope.client.protocol), + forms : $scope.client.forms, + formModel : requiredParameters, + formSubmitCallback : SUBMIT_PARAMETERS.callback, + actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ] + }); + + }; + + /** + * Returns whether the given connection state allows for submission of + * connection parameters via "argv" instructions. + * + * @param {String} connectionState + * The connection state to test, as defined by + * ManagedClientState.ConnectionState. + * + * @returns {boolean} + * true if the given connection state allows submission of connection + * parameters via "argv" instructions, false otherwise. + */ + var canSubmitParameters = function canSubmitParameters(connectionState) { + return (connectionState === ManagedClientState.ConnectionState.WAITING || + connectionState === ManagedClientState.ConnectionState.CONNECTED); + }; + + // Show status dialog when connection status changes + $scope.$watchGroup([ + 'client.clientState.connectionState', + 'client.requiredParameters', + 'client.protocol', + 'client.forms' + ], function clientStateChanged(newValues) { + + var connectionState = newValues[0]; + var requiredParameters = newValues[1]; + + // Prompt for parameters only if parameters can actually be submitted + if (requiredParameters && canSubmitParameters(connectionState)) + notifyParametersRequired(requiredParameters); + + // Otherwise, just show general connection state + else + notifyConnectionState(connectionState); + }); $scope.zoomIn = function zoomIn() { diff --git a/guacamole/src/main/webapp/app/client/styles/notification.css b/guacamole/src/main/webapp/app/client/styles/notification.css new file mode 100644 index 000000000..77a0a641a --- /dev/null +++ b/guacamole/src/main/webapp/app/client/styles/notification.css @@ -0,0 +1,23 @@ +/* + * 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. + */ + +.client .notification .parameters h3, +.client .notification .parameters .password-field .toggle-password { + display: none; +} diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js index c1eccdddc..b40a94994 100644 --- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js +++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js @@ -166,6 +166,16 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', data : '' }); + /** + * The current state of all parameters requested by the server via + * "required" instructions, where each object key is the name of a + * requested parameter and each value is the current value entered by + * the user or null if no parameters are currently being requested. + * + * @type Object. + */ + this.requiredParameters = null; + /** * All uploaded files. As files are uploaded, their progress can be * observed through the elements of this array. It is intended that @@ -578,6 +588,16 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', }); }; + // Handle any received prompts + client.onrequired = function onrequired(parameters) { + $rootScope.$apply(function promptUser() { + managedClient.requiredParameters = {}; + angular.forEach(parameters, function populateParameter(name) { + managedClient.requiredParameters[name] = ''; + }); + }); + }; + // Manage the client display managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay()); @@ -736,6 +756,29 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', delete managedClient.arguments[name]; }; + /** + * Sends the given connection parameter values using "argv" streams, + * updating the behavior of the connection in real-time if the server is + * expecting or requiring these parameters. + * + * @param {ManagedClient} managedClient + * The ManagedClient instance associated with the active connection + * being modified. + * + * @param {Object.} values + * The set of values to attempt to assign to corresponding connection + * parameters, where each object key is the connection parameter being + * set. + */ + ManagedClient.sendArguments = function sendArguments(managedClient, values) { + angular.forEach(values, function sendArgument(value, name) { + var stream = managedClient.client.createArgumentValueStream("text/plain", name); + var writer = new Guacamole.StringWriter(stream); + writer.sendText(value); + writer.sendEnd(); + }); + }; + /** * Retrieves the current values of all editable connection parameters as a * set of name/value pairs suitable for use as the model of a form which diff --git a/guacamole/src/main/webapp/app/index/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js index ac2fec451..9b9078235 100644 --- a/guacamole/src/main/webapp/app/index/controllers/indexController.js +++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js @@ -101,8 +101,9 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Broadcast keydown events keyboard.onkeydown = function onkeydown(keysym) { - // Do not handle key events if not logged in - if ($scope.expectedCredentials) + // Do not handle key events if not logged in or if a notification is + // shown + if ($scope.expectedCredentials || guacNotification.getStatus()) return true; // Warn of pending keydown @@ -119,8 +120,9 @@ angular.module('index').controller('indexController', ['$scope', '$injector', // Broadcast keyup events keyboard.onkeyup = function onkeyup(keysym) { - // Do not handle key events if not logged in - if ($scope.expectedCredentials) + // Do not handle key events if not logged in or if a notification is + // shown + if ($scope.expectedCredentials || guacNotification.getStatus()) return; // Warn of pending keyup diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json index 47e4b717b..caf1b0a2b 100644 --- a/guacamole/src/main/webapp/translations/en.json +++ b/guacamole/src/main/webapp/translations/en.json @@ -54,7 +54,9 @@ "CLIENT" : { "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Clear", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", "ACTION_DISCONNECT" : "Disconnect", "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK",