From 7a66ec9a7fc802835c1b9b2efea322ccd8c62360 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 13 Aug 2024 17:53:13 +0000 Subject: [PATCH 1/6] GUACAMOLE-1979: Allow setting required properties for connecting to MySQL 8.4 and later. --- .../MySQLAuthenticationProviderModule.java | 18 ++++++++++- .../auth/mysql/conf/MySQLEnvironment.java | 31 +++++++++++++++++++ .../mysql/conf/MySQLGuacamoleProperties.java | 27 ++++++++++++++-- 3 files changed, 73 insertions(+), 3 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java index 62cf0c4c0f..b623c77d89 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java @@ -76,6 +76,10 @@ public MySQLAuthenticationProviderModule(MySQLEnvironment environment) myBatisProperties.setProperty("mybatis.pooled.pingEnabled", "true"); myBatisProperties.setProperty("mybatis.pooled.pingQuery", "SELECT 1"); + // Set whether public key retrieval from the server is allowed + driverProperties.setProperty("allowPublicKeyRetrieval", + environment.getMYSQLAllowPublicKeyRetrieval() ? "true" : "false"); + // Use UTF-8 in database driverProperties.setProperty("characterEncoding", "UTF-8"); @@ -113,10 +117,22 @@ public MySQLAuthenticationProviderModule(MySQLEnvironment environment) if (clientPassword != null) driverProperties.setProperty("clientCertificateKeyStorePassword", clientPassword); - + // Get the MySQL-compatible driver to use. mysqlDriver = environment.getMySQLDriver(); + // Set the path to the server public key, if any + // Note that the property name casing is slightly different for MySQL + // and MariaDB drivers. See + // https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-security.html#cj-conn-prop_serverRSAPublicKeyFile + // and https://mariadb.com/kb/en/about-mariadb-connector-j/#infrequently-used-parameters + String publicKeyFile = environment.getMYSQLServerRSAPublicKeyFile(); + if (publicKeyFile != null) + driverProperties.setProperty( + mysqlDriver == MySQLDriver.MYSQL + ? "serverRSAPublicKeyFile" : "serverRsaPublicKeyFile", + publicKeyFile); + // If timezone is present, set it. TimeZone serverTz = environment.getServerTimeZone(); if (serverTz != null) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java index a3dea8964d..a75f011f18 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java @@ -442,4 +442,35 @@ public boolean enforceAccessWindowsForActiveSessions() throws GuacamoleException true); } + /** + * Returns the absolute path to the public key for the server being connected to, + * if any, or null if the configuration property is unset. + * + * @return + * The absolute path to the public key for the server being connected to. + * + * @throws GuacamoleException + * If an error occurs retrieving the configuration value. + */ + public String getMYSQLServerRSAPublicKeyFile() throws GuacamoleException { + return getProperty(MySQLGuacamoleProperties.MYSQL_SERVER_RSA_PUBLIC_KEY_FILE); + } + + /** + * Returns true if the database server public key should be automatically + * retrieved from the MySQL server, or false otherwise. + * + * @return + * Whether the database server public key should be automatically + * retrieved from the MySQL server. + * + * @throws GuacamoleException + * If an error occurs retrieving the configuration value. + */ + public boolean getMYSQLAllowPublicKeyRetrieval() throws GuacamoleException { + return getProperty( + MySQLGuacamoleProperties.MYSQL_ALLOW_PUBLIC_KEY_RETRIEVAL, + false); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java index 5e20b52c63..5f70bf1d63 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLGuacamoleProperties.java @@ -301,6 +301,29 @@ private MySQLGuacamoleProperties() {} @Override public String getName() { return "mysql-batch-size"; } - }; - + }; + + /** + * The absolute path to the public key for the server being connected to, if any. + */ + public static final StringGuacamoleProperty MYSQL_SERVER_RSA_PUBLIC_KEY_FILE = + new StringGuacamoleProperty() { + + @Override + public String getName() { return "mysql-server-rsa-public-key-file"; } + + }; + + /** + * Whether or not the server public key should be automatically retreived from + * the MySQL server. + */ + public static final BooleanGuacamoleProperty MYSQL_ALLOW_PUBLIC_KEY_RETRIEVAL = + new BooleanGuacamoleProperty() { + + @Override + public String getName() { return "mysql-allow-public-key-retrieval"; } + + }; + } From 56fb1dd59864ccce4dd5690eda121e4b4f691be4 Mon Sep 17 00:00:00 2001 From: corentin-soriano Date: Mon, 7 Oct 2024 08:32:25 +0200 Subject: [PATCH 2/6] GUACAMOLE-1989: Add the ability to open guacamole as an independent application. --- .../src/main/frontend/src/images/logo-192.png | Bin 0 -> 11107 bytes .../src/main/frontend/src/images/logo-512.png | Bin 0 -> 29134 bytes .../main/frontend/src/images/logo-vector.svg | 1 + guacamole/src/main/frontend/src/index.html | 1 + guacamole/src/main/frontend/src/manifest.json | 24 ++++++++++++++++++ guacamole/src/main/frontend/webpack.config.js | 1 + 6 files changed, 27 insertions(+) create mode 100644 guacamole/src/main/frontend/src/images/logo-192.png create mode 100644 guacamole/src/main/frontend/src/images/logo-512.png create mode 100644 guacamole/src/main/frontend/src/images/logo-vector.svg create mode 100644 guacamole/src/main/frontend/src/manifest.json diff --git a/guacamole/src/main/frontend/src/images/logo-192.png b/guacamole/src/main/frontend/src/images/logo-192.png new file mode 100644 index 0000000000000000000000000000000000000000..d5e7f6b9281b0b0141652c4d67ab5f0c1593fc4b GIT binary patch literal 11107 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 literal 0 HcmV?d00001 diff --git a/guacamole/src/main/frontend/src/images/logo-512.png b/guacamole/src/main/frontend/src/images/logo-512.png new file mode 100644 index 0000000000000000000000000000000000000000..5fcd201c2f3ee3c931e94afa44d592fdb75a3f24 GIT binary patch literal 29134 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 t?)G?-QbBe`pytzv&OTuBwSIzHIg2e*ld1C@KH| literal 0 HcmV?d00001 diff --git a/guacamole/src/main/frontend/src/images/logo-vector.svg b/guacamole/src/main/frontend/src/images/logo-vector.svg new file mode 100644 index 0000000000..2ec0cf1bcc --- /dev/null +++ b/guacamole/src/main/frontend/src/images/logo-vector.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/index.html b/guacamole/src/main/frontend/src/index.html index fd6337872a..7a26c121f6 100644 --- a/guacamole/src/main/frontend/src/index.html +++ b/guacamole/src/main/frontend/src/index.html @@ -28,6 +28,7 @@ + <% for (var index in htmlWebpackPlugin.files.css) { %> diff --git a/guacamole/src/main/frontend/src/manifest.json b/guacamole/src/main/frontend/src/manifest.json new file mode 100644 index 0000000000..8ec765339e --- /dev/null +++ b/guacamole/src/main/frontend/src/manifest.json @@ -0,0 +1,24 @@ +{ + "name": "Guacamole", + "short_name": "Guacamole", + "description": "Guacamole", + "start_url": ".", + "display": "standalone", + "icons": [ + { + "src": "./images/logo-vector.svg", + "type": "image/svg+xml", + "sizes": "512x512" + }, + { + "src": "./images/logo-512.png", + "type": "image/png", + "sizes": "512x512" + }, + { + "src": "./images/logo-192.png", + "type": "image/png", + "sizes": "192x192" + } + ] +} diff --git a/guacamole/src/main/frontend/webpack.config.js b/guacamole/src/main/frontend/webpack.config.js index dc6ad08cf5..9a96799119 100644 --- a/guacamole/src/main/frontend/webpack.config.js +++ b/guacamole/src/main/frontend/webpack.config.js @@ -137,6 +137,7 @@ module.exports = { { from: 'fonts/**/*' }, { from: 'images/**/*' }, { from: 'layouts/**/*' }, + { from: 'manifest.json' }, { from: 'translations/**/*' }, { from: 'verifyCachedVersion.js' } ], { From 32eaffce621c828dfca751575cac506d02b3df19 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Fri, 4 Oct 2024 06:16:53 -0400 Subject: [PATCH 3/6] GUACAMOLE-1701: Implement connection date and time before and after restrictions. --- .../guacamole/auth/restrict/Restrictable.java | 59 ++++++++++ .../RestrictionVerificationService.java | 104 +++++++++++++++--- .../connection/RestrictedConnection.java | 44 +------- .../RestrictedConnectionGroup.java | 44 +------- .../form/DateTimeRestrictionField.java | 100 +++++++++++++++++ .../usergroup/RestrictedUserGroup.java | 44 +------- .../main/resources/config/restrictConfig.js | 7 ++ .../dateTimeRestrictionFieldController.js | 89 +++++++++++++++ .../src/main/resources/guac-manifest.json | 5 +- .../templates/dateTimeRestrictionField.html | 12 ++ .../src/main/resources/translations/en.json | 6 + 11 files changed, 377 insertions(+), 137 deletions(-) create mode 100644 extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/DateTimeRestrictionField.java create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/controllers/dateTimeRestrictionFieldController.js create mode 100644 extensions/guacamole-auth-restrict/src/main/resources/templates/dateTimeRestrictionField.html diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/Restrictable.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/Restrictable.java index ff1acf7450..5feb21477a 100644 --- a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/Restrictable.java +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/Restrictable.java @@ -27,6 +27,65 @@ */ public interface Restrictable extends Attributes { + /** + * The name of the attribute that contains the absolute date and time after + * which this restrictable object may be used. If this attribute is present + * access to to this object will be denied at any time prior to the parsed + * value of this attribute, regardless of what other restrictions may be + * present to allow access to the object at certain days/times of the week + * or from certain hosts. + */ + public static final String RESTRICT_TIME_AFTER_ATTRIBUTE_NAME = "guac-restrict-time-after"; + + /** + * The name of the attribute that contains a list of weekdays and times (UTC) + * that this restrictable object can be used. The presence of values within + * this attribute will automatically restrict use of the object at any times + * that are not specified. + */ + public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed"; + + /** + * The name of the attribute that contains the absolute date and time before + * which use of this restrictable object may be used. If this attribute is + * present use of the object will be denied at any time after the parsed + * value of this attribute, regardless of the presence of other restrictions + * that may allow access at certain days/times of the week or from certain + * hosts. + */ + public static final String RESTRICT_TIME_BEFORE_ATTRIBUTE_NAME = "guac-restrict-time-before"; + + /** + * The name of the attribute that contains a list of weekdays and times (UTC) + * that this restrictable object cannot be used. Denied times will always take + * precedence over allowed times. The presence of this attribute without + * guac-restrict-time-allowed will deny access only during the times listed + * in this attribute, allowing access at all other times. The presence of + * this attribute along with the guac-restrict-time-allowed attribute will + * deny access at any times that overlap with the allowed times. + */ + public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied"; + + /** + * The name of the attribute that contains a list of hosts from which this + * restrictable object may be used. The presence of this attribute will + * restrict use to only users accessing Guacamole from the list of hosts + * contained in the attribute, subject to further restriction by the + * guac-restrict-hosts-denied attribute. + */ + public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed"; + + /** + * The name of the attribute that contains a list of hosts from which this + * restrictable object may not be used. The presence of this attribute, + * absent the guac-restrict-hosts-allowed attribute, will allow use from + * all hosts except the ones listed in this attribute. The presence of this + * attribute coupled with the guac-restrict-hosts-allowed attribute will + * block access from any IPs in this list, overriding any that may be + * allowed. + */ + public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied"; + /** * Return the restriction state for this restrictable object at the * current date and time. By default returns an implicit denial. diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionVerificationService.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionVerificationService.java index 41f0e9f295..ae209360f2 100644 --- a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionVerificationService.java +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/RestrictionVerificationService.java @@ -23,14 +23,14 @@ import inet.ipaddr.HostNameException; import inet.ipaddr.IPAddress; import java.net.UnknownHostException; +import java.text.ParseException; import java.util.Collection; +import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.guacamole.GuacamoleException; -import org.apache.guacamole.auth.restrict.connection.RestrictedConnection; -import org.apache.guacamole.auth.restrict.user.RestrictedUser; -import org.apache.guacamole.auth.restrict.usergroup.RestrictedUserGroup; +import org.apache.guacamole.auth.restrict.form.DateTimeRestrictionField; import org.apache.guacamole.calendar.DailyRestriction; import org.apache.guacamole.calendar.RestrictionType; import org.apache.guacamole.calendar.TimeRestrictionParser; @@ -54,6 +54,67 @@ public class RestrictionVerificationService { */ private static final Logger LOGGER = LoggerFactory.getLogger(RestrictionVerificationService.class); + /** + * Given the provided strings of an absolute date after which an action is + * valid and before which an action is valid, parse the strings into Date + * objects and determine if the current date and time falls within the + * provided window, returning the appropriate restriction type. + * + * @param afterTimeString + * The string that has the date and time value after which the activity + * is allowed. + * + * @param beforeTimeString + * The string that has the date and time value before which the activity + * is allowed. + * + * @return + * The RestrictionType that represents the allowed or denied state of + * the activity. + */ + private static RestrictionType allowedByDateTimeRestrictions( + String afterTimeString, String beforeTimeString) { + + // Set a default restriction. + RestrictionType dateTimeRestriction = RestrictionType.IMPLICIT_ALLOW; + + // Check the after string and make sure that now is after that date. + if (afterTimeString != null && !afterTimeString.isEmpty()) { + Date now = new Date(); + try { + Date afterTime = DateTimeRestrictionField.parse(afterTimeString); + if (now.before(afterTime)) + return RestrictionType.EXPLICIT_DENY; + } + catch (ParseException e) { + LOGGER.warn("Failed to parse date and time string: {}:", e.getMessage()); + LOGGER.debug("Parse exception while parsing date and time string.", e); + return RestrictionType.IMPLICIT_DENY; + } + dateTimeRestriction = RestrictionType.EXPLICIT_ALLOW; + } + + // Check the before string and make sure that now is prior to that date. + if (beforeTimeString != null && !beforeTimeString.isEmpty()) { + Date now = new Date(); + try { + Date beforeTime = DateTimeRestrictionField.parse(beforeTimeString); + if (now.after(beforeTime)) + return RestrictionType.EXPLICIT_DENY; + } + catch (ParseException e) { + LOGGER.warn("Failed to parse date and time string: {}:", e.getMessage()); + LOGGER.debug("Parse exception while parsing date and time string.", e); + return RestrictionType.IMPLICIT_DENY; + } + dateTimeRestriction = RestrictionType.EXPLICIT_ALLOW; + + } + + // Return the determined RestrictionType for the given date/time strings. + return dateTimeRestriction; + } + /** * Parse out the provided strings of allowed and denied times, verifying * whether or not a login or connection should be allowed at the current @@ -251,8 +312,8 @@ public static void verifyHostRestrictions(UserContext context, Map userAttributes = currentUser.getAttributes(); // Verify host-based restrictions specific to the user - String allowedHostString = userAttributes.get(RestrictedUser.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME); - String deniedHostString = userAttributes.get(RestrictedUser.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME); + String allowedHostString = userAttributes.get(Restrictable.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME); + String deniedHostString = userAttributes.get(Restrictable.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME); RestrictionType hostRestrictionResult = allowedByHostRestrictions(allowedHostString, deniedHostString, remoteAddress); switch (hostRestrictionResult) { @@ -284,8 +345,8 @@ public static void verifyHostRestrictions(UserContext context, Map grpAttributes = userGroup.getAttributes(); // Pull host-based restrictions for this group and verify - String grpAllowedHostString = grpAttributes.get(RestrictedUserGroup.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME); - String grpDeniedHostString = grpAttributes.get(RestrictedUserGroup.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME); + String grpAllowedHostString = grpAttributes.get(Restrictable.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME); + String grpDeniedHostString = grpAttributes.get(Restrictable.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME); RestrictionType grpRestrictionResult = allowedByHostRestrictions(grpAllowedHostString, grpDeniedHostString, remoteAddress); // Any explicit denials are thrown immediately @@ -344,8 +405,8 @@ public static void verifyHostRestrictions(Restrictable restrictable, String remoteAddress) throws GuacamoleException { // Verify time-based restrictions specific to this connection. - String allowedHostsString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME); - String deniedHostsString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME); + String allowedHostsString = restrictable.getAttributes().get(Restrictable.RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME); + String deniedHostsString = restrictable.getAttributes().get(Restrictable.RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME); RestrictionType hostRestrictionResult = allowedByHostRestrictions(allowedHostsString, deniedHostsString, remoteAddress); // If the host is not allowed @@ -393,8 +454,8 @@ public static void verifyTimeRestrictions(UserContext context, Map userAttributes = currentUser.getAttributes(); // Verify time-based restrictions specific to the user - String allowedTimeString = userAttributes.get(RestrictedUser.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME); - String deniedTimeString = userAttributes.get(RestrictedUser.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME); + String allowedTimeString = userAttributes.get(Restrictable.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME); + String deniedTimeString = userAttributes.get(Restrictable.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME); RestrictionType timeRestrictionResult = allowedByTimeRestrictions(allowedTimeString, deniedTimeString); // Check the time restriction for explicit results. @@ -426,8 +487,8 @@ public static void verifyTimeRestrictions(UserContext context, Map grpAttributes = userGroup.getAttributes(); // Pull time-based restrictions for this group and verify - String grpAllowedTimeString = grpAttributes.get(RestrictedUserGroup.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME); - String grpDeniedTimeString = grpAttributes.get(RestrictedUserGroup.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME); + String grpAllowedTimeString = grpAttributes.get(Restrictable.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME); + String grpDeniedTimeString = grpAttributes.get(Restrictable.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME); RestrictionType grpRestrictionResult = allowedByTimeRestrictions(grpAllowedTimeString, grpDeniedTimeString); // An explicit deny results in immediate denial of the login. @@ -463,6 +524,18 @@ public static void verifyTimeRestrictions(UserContext context, } + public static void verifyDateTimeRestrictions(Restrictable restrictable) throws GuacamoleException { + + String afterTimeString = restrictable.getAttributes().get(Restrictable.RESTRICT_TIME_AFTER_ATTRIBUTE_NAME); + String beforeTimeString = restrictable.getAttributes().get(Restrictable.RESTRICT_TIME_BEFORE_ATTRIBUTE_NAME); + RestrictionType dateRestriction = allowedByDateTimeRestrictions(afterTimeString, beforeTimeString); + if (!dateRestriction.isAllowed()) + throw new TranslatableGuacamoleSecurityException( + "Use of this connection or connection group is not allowed at this time.", + "RESTRICT.ERROR_CONNECTION_NOT_ALLOWED_NOW" + ); + } + /** * Verify the time restrictions for the given Connection object, throwing * an exception if the connection should not be allowed, or silently @@ -478,8 +551,8 @@ public static void verifyTimeRestrictions(UserContext context, public static void verifyTimeRestrictions(Restrictable restrictable) throws GuacamoleException { // Verify time-based restrictions specific to this connection. - String allowedTimeString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME); - String deniedTimeString = restrictable.getAttributes().get(RestrictedConnection.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME); + String allowedTimeString = restrictable.getAttributes().get(Restrictable.RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME); + String deniedTimeString = restrictable.getAttributes().get(Restrictable.RESTRICT_TIME_DENIED_ATTRIBUTE_NAME); RestrictionType timeRestriction = allowedByTimeRestrictions(allowedTimeString, deniedTimeString); if (!timeRestriction.isAllowed()) throw new TranslatableGuacamoleSecurityException( @@ -536,6 +609,7 @@ public static void verifyLoginRestrictions(UserContext context, */ public static void verifyConnectionRestrictions(Restrictable restrictable, String remoteAddress) throws GuacamoleException { + verifyDateTimeRestrictions(restrictable); verifyTimeRestrictions(restrictable); verifyHostRestrictions(restrictable, remoteAddress); } diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictedConnection.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictedConnection.java index bdbce0bcc4..08adee54a6 100644 --- a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictedConnection.java +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connection/RestrictedConnection.java @@ -26,6 +26,7 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.restrict.Restrictable; import org.apache.guacamole.auth.restrict.RestrictionVerificationService; +import org.apache.guacamole.auth.restrict.form.DateTimeRestrictionField; import org.apache.guacamole.auth.restrict.form.HostRestrictionField; import org.apache.guacamole.auth.restrict.form.TimeRestrictionField; import org.apache.guacamole.calendar.RestrictionType; @@ -46,49 +47,12 @@ public class RestrictedConnection extends DelegatingConnection implements Restri */ private final String remoteAddress; - /** - * The name of the attribute that contains a list of weekdays and times (UTC) - * that this connection can be accessed. The presence of values within this - * attribute will automatically restrict use of the connections at any - * times that are not specified. - */ - public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed"; - - /** - * The name of the attribute that contains a list of weekdays and times (UTC) - * that this connection cannot be accessed. Denied times will always take - * precedence over allowed times. The presence of this attribute without - * guac-restrict-time-allowed will deny access only during the times listed - * in this attribute, allowing access at all other times. The presence of - * this attribute along with the guac-restrict-time-allowed attribute will - * deny access at any times that overlap with the allowed times. - */ - public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied"; - - /** - * The name of the attribute that contains a list of hosts from which a user - * may access this connection. The presence of this attribute will restrict - * access to only users accessing Guacamole from the list of hosts contained - * in the attribute, subject to further restriction by the - * guac-restrict-hosts-denied attribute. - */ - public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed"; - - /** - * The name of the attribute that contains a list of hosts from which - * a user may not access this connection. The presence of this attribute, - * absent the guac-restrict-hosts-allowed attribute, will allow access from - * all hosts except the ones listed in this attribute. The presence of this - * attribute coupled with the guac-restrict-hosts-allowed attribute will - * block access from any IPs in this list, overriding any that may be - * allowed. - */ - public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied"; - /** * The list of all connection attributes provided by this Connection implementation. */ public static final List RESTRICT_CONNECTION_ATTRIBUTES = Arrays.asList( + RESTRICT_TIME_AFTER_ATTRIBUTE_NAME, + RESTRICT_TIME_BEFORE_ATTRIBUTE_NAME, RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME, RESTRICT_TIME_DENIED_ATTRIBUTE_NAME, RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME, @@ -101,6 +65,8 @@ public class RestrictedConnection extends DelegatingConnection implements Restri */ public static final Form RESTRICT_CONNECTION_FORM = new Form("restrict-login-form", Arrays.asList( + new DateTimeRestrictionField(RESTRICT_TIME_AFTER_ATTRIBUTE_NAME), + new DateTimeRestrictionField(RESTRICT_TIME_BEFORE_ATTRIBUTE_NAME), new TimeRestrictionField(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME), new TimeRestrictionField(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME), new HostRestrictionField(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME), diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictedConnectionGroup.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictedConnectionGroup.java index b6c18144ef..1f61a26363 100644 --- a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictedConnectionGroup.java +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/connectiongroup/RestrictedConnectionGroup.java @@ -26,6 +26,7 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.auth.restrict.Restrictable; import org.apache.guacamole.auth.restrict.RestrictionVerificationService; +import org.apache.guacamole.auth.restrict.form.DateTimeRestrictionField; import org.apache.guacamole.auth.restrict.form.HostRestrictionField; import org.apache.guacamole.auth.restrict.form.TimeRestrictionField; import org.apache.guacamole.calendar.RestrictionType; @@ -46,50 +47,13 @@ public class RestrictedConnectionGroup extends DelegatingConnectionGroup impleme */ private final String remoteAddress; - /** - * The name of the attribute that contains a list of weekdays and times (UTC) - * that this connection group can be accessed. The presence of values within - * this attribute will automatically restrict use of the connection group - * at any times that are not specified. - */ - public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed"; - - /** - * The name of the attribute that contains a list of weekdays and times (UTC) - * that this connection group cannot be accessed. Denied times will always - * take precedence over allowed times. The presence of this attribute without - * guac-restrict-time-allowed will deny access only during the times listed - * in this attribute, allowing access at all other times. The presence of - * this attribute along with the guac-restrict-time-allowed attribute will - * deny access at any times that overlap with the allowed times. - */ - public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied"; - - /** - * The name of the attribute that contains a list of hosts from which a user - * may access this connection group. The presence of this attribute will - * restrict access to only users accessing Guacamole from the list of hosts - * contained in the attribute, subject to further restriction by the - * guac-restrict-hosts-denied attribute. - */ - public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed"; - - /** - * The name of the attribute that contains a list of hosts from which - * a user may not access this connection group. The presence of this - * attribute, absent the guac-restrict-hosts-allowed attribute, will allow - * access from all hosts except the ones listed in this attribute. The - * presence of this attribute coupled with the guac-restrict-hosts-allowed - * attribute will block access from any hosts in this list, overriding any - * that may be allowed. - */ - public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied"; - /** * The list of all connection group attributes provided by this * ConnectionGroup implementation. */ public static final List RESTRICT_CONNECTIONGROUP_ATTRIBUTES = Arrays.asList( + RESTRICT_TIME_AFTER_ATTRIBUTE_NAME, + RESTRICT_TIME_BEFORE_ATTRIBUTE_NAME, RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME, RESTRICT_TIME_DENIED_ATTRIBUTE_NAME, RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME, @@ -102,6 +66,8 @@ public class RestrictedConnectionGroup extends DelegatingConnectionGroup impleme */ public static final Form RESTRICT_CONNECTIONGROUP_FORM = new Form("restrict-login-form", Arrays.asList( + new DateTimeRestrictionField(RESTRICT_TIME_AFTER_ATTRIBUTE_NAME), + new DateTimeRestrictionField(RESTRICT_TIME_BEFORE_ATTRIBUTE_NAME), new TimeRestrictionField(RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME), new TimeRestrictionField(RESTRICT_TIME_DENIED_ATTRIBUTE_NAME), new HostRestrictionField(RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME), diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/DateTimeRestrictionField.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/DateTimeRestrictionField.java new file mode 100644 index 0000000000..91ba5724b2 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/form/DateTimeRestrictionField.java @@ -0,0 +1,100 @@ +/* + * 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. + */ + +package org.apache.guacamole.auth.restrict.form; + +import java.text.DateFormat; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import org.apache.guacamole.form.Field; + +/** + * A field that parses a string containing an absolute date and time value. + */ +public class DateTimeRestrictionField extends Field { + + /** + * The field type. + */ + public static final String FIELD_TYPE = "GUAC_DATETIME_RESTRICTION"; + + /** + * The format of the data for this field as it will be stored in the + * underlying storage mechanism. + */ + public static final String FORMAT = "yyyy-MM-dd'T'HH:mm:ssZ"; + + /** + * Create a new field that tracks time restrictions. + * + * @param name + * The name of the parameter that will be used to pass this field + * between the REST API and the web front-end. + * + */ + public DateTimeRestrictionField(String name) { + super(name, FIELD_TYPE); + } + + /** + * Converts the given date into a string which follows the format used by + * date fields. + * + * @param date + * The date value to format, which may be null. + * + * @return + * The formatted date, or null if the provided time was null. + */ + public static String format(Date date) { + DateFormat dateFormat = new SimpleDateFormat(DateTimeRestrictionField.FORMAT); + return date == null ? null : dateFormat.format(date); + } + + /** + * Parses the given string into a corresponding date. The string must + * follow the standard format used by date fields, as defined by FORMAT + * and as would be produced by format(). + * + * @param dateString + * The date string to parse, which may be null. + * + * @return + * The date corresponding to the given date string, or null if the + * provided date string was null or blank. + * + * @throws ParseException + * If the given date string does not conform to the standard format + * used by date fields. + */ + public static Date parse(String dateString) + throws ParseException { + + // Return null if no date provided + if (dateString == null || dateString.isEmpty()) + return null; + + // Parse date according to format + DateFormat dateFormat = new SimpleDateFormat(DateTimeRestrictionField.FORMAT); + return dateFormat.parse(dateString); + + } + +} diff --git a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictedUserGroup.java b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictedUserGroup.java index 2e637872b9..6d2d6c51e0 100644 --- a/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictedUserGroup.java +++ b/extensions/guacamole-auth-restrict/src/main/java/org/apache/guacamole/auth/restrict/usergroup/RestrictedUserGroup.java @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import org.apache.guacamole.auth.restrict.Restrictable; import org.apache.guacamole.auth.restrict.form.HostRestrictionField; import org.apache.guacamole.auth.restrict.form.TimeRestrictionField; import org.apache.guacamole.form.Form; @@ -33,48 +34,7 @@ * UserGroup implementation which wraps a UserGroup from another extension and * enforces additional restrictions for members of that group. */ -public class RestrictedUserGroup extends DelegatingUserGroup { - - /** - * The name of the attribute that contains a list of weekdays and times (UTC) - * that members of a group are allowed to log in. The presence of this - * attribute will restrict any users who are members of the group to logins - * only during the times that are contained within the attribute, - * subject to further restriction by the guac-restrict-time-denied attribute. - */ - public static final String RESTRICT_TIME_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-time-allowed"; - - /** - * The name of the attribute that contains a list of weekdays and times (UTC) - * that members of a group are not allowed to log in. Denied times will - * always take precedence over allowed times. The presence of this attribute - * without guac-restrict-time-allowed will deny logins only during the times - * listed in this attribute, allowing logins at all other times. The - * presence of this attribute along with the guac-restrict-time-allowed - * attribute will deny logins at any times that overlap with the allowed - * times. - */ - public static final String RESTRICT_TIME_DENIED_ATTRIBUTE_NAME = "guac-restrict-time-denied"; - - /** - * The name of the attribute that contains a list of IP addresses from which - * members of a group are allowed to log in. The presence of this attribute - * will restrict users to only the list of IP addresses contained in the - * attribute, subject to further restriction by the - * guac-restrict-hosts-denied attribute. - */ - public static final String RESTRICT_HOSTS_ALLOWED_ATTRIBUTE_NAME = "guac-restrict-hosts-allowed"; - - /** - * The name of the attribute that contains a list of IP addresses from which - * members of a group are not allowed to log in. The presence of this - * attribute, absent the guac-restrict-hosts-allowed attribute, will allow - * logins from all hosts except the ones listed in this attribute. The - * presence of this attribute coupled with the guac-restrict-hosts-allowed - * attribute will block access from any IPs in this list, overriding any - * that may be allowed. - */ - public static final String RESTRICT_HOSTS_DENIED_ATTRIBUTE_NAME = "guac-restrict-hosts-denied"; +public class RestrictedUserGroup extends DelegatingUserGroup implements Restrictable { /** * The list of all user attributes provided by this UserGroup implementation. diff --git a/extensions/guacamole-auth-restrict/src/main/resources/config/restrictConfig.js b/extensions/guacamole-auth-restrict/src/main/resources/config/restrictConfig.js index 4c63e8a2dd..953b9b9106 100644 --- a/extensions/guacamole-auth-restrict/src/main/resources/config/restrictConfig.js +++ b/extensions/guacamole-auth-restrict/src/main/resources/config/restrictConfig.js @@ -36,5 +36,12 @@ angular.module('guacRestrict').config(['formServiceProvider', controller : 'hostRestrictionFieldController', templateUrl : 'app/ext/restrict/templates/hostRestrictionField.html' }); + + // Define the date and time restriction field + formServiceProvider.registerFieldType('GUAC_DATETIME_RESTRICTION', { + module : 'guacRestrict', + controller : 'dateTimeRestrictionFieldController', + templateUrl : 'app/ext/restrict/templates/dateTimeRestrictionField.html' + }); }]); diff --git a/extensions/guacamole-auth-restrict/src/main/resources/controllers/dateTimeRestrictionFieldController.js b/extensions/guacamole-auth-restrict/src/main/resources/controllers/dateTimeRestrictionFieldController.js new file mode 100644 index 0000000000..6fd2054565 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/controllers/dateTimeRestrictionFieldController.js @@ -0,0 +1,89 @@ +/* + * 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. + */ + + +/** + * Controller for date+time restriction fields. + */ +angular.module('form').controller('dateTimeRestrictionFieldController', + ['$scope', '$injector', + function dateTimeRestrictionFieldController($scope, $injector) { + + // Required services + var $filter = $injector.get('$filter'); + + /** + * Options which dictate the behavior of the input field model, as defined + * by https://docs.angularjs.org/api/ng/directive/ngModelOptions + * + * @type Object. + */ + $scope.modelOptions = { + + /** + * Space-delimited list of events on which the model will be updated. + * + * @type String + */ + updateOn : 'blur', + + /** + * The time zone to use when reading/writing the Date object of the + * model. + * + * @type String + */ + timezone : 'UTC' + + }; + + /** + * Parses the date and time components of the given string into a Date in + * the UTC timezone. The input string must be in the format + * YYYY-MM-DDTHH:mm:ss (zero-padded). + * + * @param {String} str + * The date+time string to parse. + * + * @returns {Date} + * A Date object, in the UTC timezone, or null if parsing the provided + * string fails. + */ + var parseDate = function parseDate(str) { + + // Parse date, return null if parsing fails + var parsedDate = new Date(str); + if (isNaN(parsedDate.getTime())) + return null; + + return parsedDate; + + }; + + // Update typed value when model is changed + $scope.$watch('model', function modelChanged(model) { + $scope.typedValue = (model ? parseDate(model) : null); + }); + + // Update string value in model when typed value is changed + $scope.$watch('typedValue', function typedValueChanged(typedValue) { + $scope.model = (typedValue ? $filter('date')(typedValue, 'yyyy-MM-ddTHH:mm:ssZ', 'UTC') : ''); + }); + +}]); diff --git a/extensions/guacamole-auth-restrict/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-restrict/src/main/resources/guac-manifest.json index e0b6928483..7391e23b94 100644 --- a/extensions/guacamole-auth-restrict/src/main/resources/guac-manifest.json +++ b/extensions/guacamole-auth-restrict/src/main/resources/guac-manifest.json @@ -22,8 +22,9 @@ ], "resources" : { - "templates/hostRestrictionField.html" : "text/html", - "templates/timeRestrictionField.html" : "text/html" + "templates/dateTimeRestrictionField.html" : "text/html", + "templates/hostRestrictionField.html" : "text/html", + "templates/timeRestrictionField.html" : "text/html" } } diff --git a/extensions/guacamole-auth-restrict/src/main/resources/templates/dateTimeRestrictionField.html b/extensions/guacamole-auth-restrict/src/main/resources/templates/dateTimeRestrictionField.html new file mode 100644 index 0000000000..8630761fd7 --- /dev/null +++ b/extensions/guacamole-auth-restrict/src/main/resources/templates/dateTimeRestrictionField.html @@ -0,0 +1,12 @@ +
+ +
\ No newline at end of file diff --git a/extensions/guacamole-auth-restrict/src/main/resources/translations/en.json b/extensions/guacamole-auth-restrict/src/main/resources/translations/en.json index 3eb3d04603..2f52d4097b 100644 --- a/extensions/guacamole-auth-restrict/src/main/resources/translations/en.json +++ b/extensions/guacamole-auth-restrict/src/main/resources/translations/en.json @@ -8,7 +8,9 @@ "FIELD_HEADER_GUAC_RESTRICT_HOSTS_ALLOWED" : "Hosts from which connection may be accessed:", "FIELD_HEADER_GUAC_RESTRICT_HOSTS_DENIED" : "Hosts from which connection may not be accessed:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_AFTER" : "Date and time after which this connection may be used:", "FIELD_HEADER_GUAC_RESTRICT_TIME_ALLOWED" : "Times connection is allowed to be used:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_BEFORE" : "Date and time before which this connection may be used:", "FIELD_HEADER_GUAC_RESTRICT_TIME_DENIED" : "Times connection may not be used:", "SECTION_HEADER_RESTRICT_LOGIN_FORM" : "Additional Connection Restrictions" @@ -19,7 +21,9 @@ "FIELD_HEADER_GUAC_RESTRICT_HOSTS_ALLOWED" : "Hosts from which connection group may be accessed:", "FIELD_HEADER_GUAC_RESTRICT_HOSTS_DENIED" : "Hosts from which connection group may not be accessed:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_AFTER" : "Date and time after which this connection group may be used:", "FIELD_HEADER_GUAC_RESTRICT_TIME_ALLOWED" : "Times connection group is allowed to be used:", + "FIELD_HEADER_GUAC_RESTRICT_TIME_BEFORE" : "Date and time before which this connection group may be used:", "FIELD_HEADER_GUAC_RESTRICT_TIME_DENIED" : "Times connection group may not be used:", "SECTION_HEADER_RESTRICT_LOGIN_FORM" : "Additional Connection Restrictions" @@ -35,6 +39,8 @@ "ERROR_USER_LOGIN_NOT_ALLOWED_NOW" : "The login for this user is not allowed at this time.", "ERROR_USER_LOGIN_NOT_ALLOWED_FROM_HOST" : "The login for this user is not allowed from this host.", + "FIELD_PLACEHOLDER_DATE_TIME_RESTRICTION" : "YYYY-MM-DD HH:MM:SS", + "TABLE_HEADER_DAY" : "Day", "TABLE_HEADER_END_TIME" : "End Time", "TABLE_HEADER_HOST" : "Host", From 8d1bf0393f7caa1363596ea694e60cae1ea384f1 Mon Sep 17 00:00:00 2001 From: Virtually Nick Date: Fri, 9 Aug 2024 10:21:00 -0400 Subject: [PATCH 4/6] GUACAMOLE-1976: Add OPTIONAL token modifier. --- .../apache/guacamole/token/TokenFilter.java | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java b/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java index a9766face3..673e134d90 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java @@ -24,6 +24,8 @@ import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Filtering object which replaces tokens of the form "${TOKEN_NAME}" with @@ -33,6 +35,11 @@ */ public class TokenFilter { + /** + * The logger for this class. + */ + private static final Logger LOGGER = LoggerFactory.getLogger(TokenFilter.class); + /** * Regular expression which matches individual tokens, with additional * capturing groups for convenient retrieval of leading text, the possible @@ -225,6 +232,17 @@ private String filter(String input, boolean strict) // strict mode is enabled if (tokenValue == null) { + // Token marked as optional, so just skip it and update + // last match. + if (modifier != null && modifier.equals("OPTIONAL")) { + LOGGER.debug("The token \"{}\" has no value and has been " + + "marked as optional, so it will be treated " + + "as a blank value instead of a literal.", + tokenName); + endOfLastMatch = tokenMatcher.end(); + continue; + } + // Fail outright if strict mode is enabled if (strict) throw new GuacamoleTokenUndefinedException("Token " @@ -232,8 +250,7 @@ private String filter(String input, boolean strict) // If strict mode is NOT enabled, simply interpret as // a literal - String notToken = tokenMatcher.group(TOKEN_GROUP); - output.append(notToken); + output.append(tokenMatcher.group(TOKEN_GROUP)); } From 2770ef3a37961ff00b355826829c031b55a79493 Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Tue, 30 Jul 2024 23:03:24 +0000 Subject: [PATCH 5/6] GUACAMOLE-1974: Allow deferring received pipe streams for later consumption by name-specific handlers. --- .../src/app/client/types/ManagedClient.js | 140 +++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js b/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js index 01769ea94f..b7e83a3a05 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js +++ b/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js @@ -62,12 +62,49 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', */ var THUMBNAIL_UPDATE_FREQUENCY = 5000; + /** + * A deferred pipe stream, that has yet to be consumed, as well as all + * axuilary information needed to pull data from the stream. + * + * @constructor + * @param {DeferredPipeStream|Object} [template={}] + * The object whose properties should be copied within the new + * DeferredPipeStream. + */ + var DeferredPipeStream = function DeferredPipeStream(template) { + + // Use empty object by default + template = template || {}; + + /** + * The stream that will receive data from the server. + * + * @type Guacamole.InputStream + */ + this.stream = template.stream; + + /** + * The mimetype of the data which will be received. + * + * @type String + */ + this.mimetype = template.mimetype; + + /** + * The name of the pipe. + * + * @type String + */ + this.name = template.name; + + }; + /** * Object which serves as a surrogate interface, encapsulating a Guacamole * client while it is active, allowing it to be maintained in the * background. One or more ManagedClients are grouped within * ManagedClientGroups before being attached to the client view. - * + * * @constructor * @param {ManagedClient|Object} [template={}] * The object whose properties should be copied within the new @@ -240,6 +277,22 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', */ this.arguments = template.arguments || {}; + /** + * Any received pipe streams that have not been consumed by an onpipe + * handler or registered pipe handler, indexed by pipe stream name. + * + * @type {Object.} + */ + this.deferredPipeStreams = template.deferredPipeStreams || {}; + + /** + * Handlers for deferred pipe streams, indexed by the name of the pipe + * stream that the handler should handle. + * + * @type {Object.} + */ + this.deferredPipeStreamHandlers = template.deferredPipeStreamHandlers || {}; + }; /** @@ -553,6 +606,25 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', }; + // A default onpipe implementation that will automatically defer any + // received pipe streams, automatically invoking any registered handlers + // that may already be set for the received name + client.onpipe = (stream, mimetype, name) => { + + // Defer the pipe stream + managedClient.deferredPipeStreams[name] = new DeferredPipeStream( + { stream, mimetype, name }); + + // Invoke the handler now, if set + const handler = managedClient.deferredPipeStreamHandlers[name]; + if (handler) { + + // Handle the stream, and clear from the deferred streams + handler(stream, mimetype, name); + delete managedClient.deferredPipeStreams[name]; + } + }; + // Test for argument mutability whenever an argument value is // received client.onargv = function clientArgumentValueReceived(stream, mimetype, name) { @@ -1004,6 +1076,72 @@ angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', }; + + /** + * Register a handler that will be automatically invoked for any deferred + * pipe stream with the provided name, either when a pipe stream with a + * name matching a registered handler is received, or immediately when this + * function is called, if such a pipe stream has already been received. + * + * NOTE: Pipe streams are automatically deferred by the default onpipe + * implementation. To preserve this behavior when using a custom onpipe + * callback, make sure to defer to the default implementation as needed. + * + * @param {ManagedClient} managedClient + * The client for which the deferred pipe stream handler should be set. + * + * @param {String} name + * The name of the pipe stream that should be handeled by the provided + * handler. If another handler is already registered for this name, it + * will be replaced by the handler provided to this function. + * + * @param {Function} handler + * The handler that should handle any deferred pipe stream with the + * provided name. This function must take the same arguments as the + * standard onpipe handler - namely, the stream itself, the mimetype, + * and the name. + */ + ManagedClient.registerDeferredPipeHandler = function registerDeferredPipeHandler( + managedClient, name, handler) { + managedClient.deferredPipeStreamHandlers[name] = handler; + + // Invoke the handler now, if the pipestream has already been received + if (managedClient.deferredPipeStreams[name]) { + + // Invoke the handler with the deferred pipe stream + var deferredStream = managedClient.deferredPipeStreams[name]; + handler(deferredStream.stream, + deferredStream.mimetype, + deferredStream.name); + + // Clean up the now-consumed pipe stream + delete managedClient.deferredPipeStreams[name]; + } + }; + + /** + * Detach the provided deferred pipe stream handler, if it is currently + * registered for the provided pipe stream name. + * + * @param {String} name + * The name of the associated pipe stream for the handler that should + * be detached. + * + * @param {Function} handler + * The handler that should be detached. + * + * @param {ManagedClient} managedClient + * The client for which the deferred pipe stream handler should be + * detached. + */ + ManagedClient.detachDeferredPipeHandler = function detachDeferredPipeHandler( + managedClient, name, handler) { + + // Remove the handler if found + if (managedClient.deferredPipeStreamHandlers[name] === handler) + delete managedClient.deferredPipeStreamHandlers[name]; + }; + return ManagedClient; }]); From 85abb6ebbb561afa8f02ff5363bf1ddeb83f7f5c Mon Sep 17 00:00:00 2001 From: VAGurko <123146241+VAGurko@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:39:38 +0400 Subject: [PATCH 6/6] =?UTF-8?q?GUACAMOLE-2013:=20There=20is=20a=20typo=20i?= =?UTF-8?q?n=20the=20Russian=20translation=20in=20the=20Guacamole=20interf?= =?UTF-8?q?ace=20"=D1=81=D0=B5=D0=BA=D1=83=D0=BD=D1=8B",=20it=20needs=20to?= =?UTF-8?q?=20be=20fixed=20to=20"=D1=81=D0=B5=D0=BA=D1=83=D0=BD=D0=B4?= =?UTF-8?q?=D1=8B".?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In the file "guacamole/src/main/frontend/src/translations/ru.json" the typo of "секуны" has been corrected to "секунды". --- guacamole/src/main/frontend/src/translations/ru.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/guacamole/src/main/frontend/src/translations/ru.json b/guacamole/src/main/frontend/src/translations/ru.json index a086f73390..e5e61b7830 100644 --- a/guacamole/src/main/frontend/src/translations/ru.json +++ b/guacamole/src/main/frontend/src/translations/ru.json @@ -43,7 +43,7 @@ "INFO_ACTIVE_USER_COUNT" : "Сейчас в системе {USERS} {USERS, plural, one{пользователь} few{пользователя} many{пользователей} other{пользователя}}.", "TEXT_ANONYMOUS_USER" : "Аноним", - "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{секунда} few{секуны} many{секунд} other{секуны}}} minute{{VALUE, plural, one{минута} few{минуты} many{минут} other{минуты}}} hour{{VALUE, plural, one{час} few{часа} many{часов} other{часа}}} day{{VALUE, plural, one{день} few{дня} many{дней} other{дня}}} other{}}" + "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{секунда} few{секунды} many{секунд} other{секунды}}} minute{{VALUE, plural, one{минута} few{минуты} many{минут} other{минуты}}} hour{{VALUE, plural, one{час} few{часа} many{часов} other{часа}}} day{{VALUE, plural, one{день} few{дня} many{дней} other{дня}}} other{}}" },