GUACAMOLE-514: Merge parameter definition and translations for VNC "username" field.
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java
index 1d3344d..c21e9c3 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionService.java
@@ -91,15 +91,16 @@
Collection<TrackedActiveConnection> activeConnections = new ArrayList<TrackedActiveConnection>(identifiers.size());
for (ActiveConnectionRecord record : records) {
- // Sensitive information should be included if the connection was
- // started by the current user OR the user is an admin
- boolean includeSensitiveInformation =
+ // The current user should have access to sensitive information and
+ // be able to connect to (join) the active connection if they are
+ // the user that started the connection OR the user is an admin
+ boolean hasPrivilegedAccess =
isAdmin || username.equals(record.getUsername());
// Add connection if within requested identifiers
if (identifierSet.contains(record.getUUID().toString())) {
TrackedActiveConnection activeConnection = trackedActiveConnectionProvider.get();
- activeConnection.init(user, record, includeSensitiveInformation);
+ activeConnection.init(user, record, hasPrivilegedAccess, hasPrivilegedAccess);
activeConnections.add(activeConnection);
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java
index 5424550..cdbcc07 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/TrackedActiveConnection.java
@@ -21,15 +21,20 @@
import com.google.inject.Inject;
import java.util.Date;
+import java.util.Map;
import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.auth.jdbc.base.RestrictedObject;
import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
import org.apache.guacamole.auth.jdbc.sharing.ConnectionSharingService;
+import org.apache.guacamole.auth.jdbc.sharing.connection.SharedConnectionDefinition;
import org.apache.guacamole.auth.jdbc.tunnel.ActiveConnectionRecord;
+import org.apache.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService;
import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.ActiveConnection;
import org.apache.guacamole.net.auth.credentials.UserCredentials;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
/**
* An implementation of the ActiveConnection object which has an associated
@@ -44,6 +49,12 @@
private ConnectionSharingService sharingService;
/**
+ * Service for creating and tracking tunnels.
+ */
+ @Inject
+ private GuacamoleTunnelService tunnelService;
+
+ /**
* The identifier of this active connection.
*/
private String identifier;
@@ -85,6 +96,11 @@
private GuacamoleTunnel tunnel;
/**
+ * Whether connections to this TrackedActiveConnection are allowed.
+ */
+ private boolean connectable;
+
+ /**
* Initializes this TrackedActiveConnection, copying the data associated
* with the given active connection record. At a minimum, the identifier
* of this active connection will be set, the start date, and the
@@ -102,13 +118,19 @@
* Whether sensitive data should be copied from the connection record
* as well. This includes the remote host, associated tunnel, and
* username.
+ *
+ * @param connectable
+ * Whether the user that retrieved this object should be allowed to
+ * join the active connection.
*/
public void init(ModeledAuthenticatedUser currentUser,
ActiveConnectionRecord activeConnectionRecord,
- boolean includeSensitiveInformation) {
+ boolean includeSensitiveInformation,
+ boolean connectable) {
super.init(currentUser);
this.connectionRecord = activeConnectionRecord;
+ this.connectable = connectable;
// Copy all non-sensitive data from given record
this.connection = activeConnectionRecord.getConnection();
@@ -169,11 +191,32 @@
this.sharingProfileIdentifier = sharingProfileIdentifier;
}
+ /**
+ * Shares this active connection with the user that retrieved it, returning
+ * a SharedConnectionDefinition that can be used to establish a tunnel to
+ * the shared connection. If provided, access within the shared connection
+ * will be restricted by the sharing profile with the given identifier.
+ *
+ * @param identifier
+ * The identifier of the sharing profile that defines the restrictions
+ * applying to the shared connection, or null if no such restrictions
+ * apply.
+ *
+ * @return
+ * A new SharedConnectionDefinition which can be used to establish a
+ * tunnel to the shared connection.
+ *
+ * @throws GuacamoleException
+ * If permission to share this active connection is denied.
+ */
+ private SharedConnectionDefinition share(String identifier) throws GuacamoleException {
+ return sharingService.shareConnection(getCurrentUser(), connectionRecord, identifier);
+ }
+
@Override
public UserCredentials getSharingCredentials(String identifier)
throws GuacamoleException {
- return sharingService.generateTemporaryCredentials(getCurrentUser(),
- connectionRecord, identifier);
+ return sharingService.getSharingCredentials(share(identifier));
}
@Override
@@ -216,4 +259,26 @@
this.tunnel = tunnel;
}
+ @Override
+ public boolean isConnectable() {
+ return connectable;
+ }
+
+ @Override
+ public GuacamoleTunnel connect(GuacamoleClientInformation info,
+ Map<String, String> tokens) throws GuacamoleException {
+
+ // Establish connection only if connecting is allowed
+ if (isConnectable())
+ return tunnelService.getGuacamoleTunnel(getCurrentUser(), share(null), info, tokens);
+
+ throw new GuacamoleSecurityException("Permission denied.");
+
+ }
+
+ @Override
+ public int getActiveConnections() {
+ return 0;
+ }
+
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/ConnectionSharingService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/ConnectionSharingService.java
index efdecf0..fe038b6 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/ConnectionSharingService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/ConnectionSharingService.java
@@ -21,6 +21,7 @@
import com.google.inject.Inject;
import java.util.Collections;
+import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
import org.apache.guacamole.GuacamoleException;
@@ -30,11 +31,14 @@
import org.apache.guacamole.auth.jdbc.sharingprofile.ModeledSharingProfile;
import org.apache.guacamole.auth.jdbc.sharingprofile.SharingProfileService;
import org.apache.guacamole.auth.jdbc.tunnel.ActiveConnectionRecord;
+import org.apache.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService;
+import org.apache.guacamole.auth.jdbc.user.RemoteAuthenticatedUser;
import org.apache.guacamole.form.Field;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.UserCredentials;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
/**
* Service which provides convenience methods for sharing active connections.
@@ -75,10 +79,16 @@
));
/**
- * Generates a set of temporary credentials which can be used to connect to
- * the given connection using the given sharing profile. If the user does
- * not have permission to share the connection via the given sharing
- * profile, permission will be denied.
+ * Creates a new SharedConnectionDefinition which can be used to connect to
+ * the given connection, optionally restricting access to the shared
+ * connection using the given sharing profile. If the user does not have
+ * permission to share the connection via the given sharing profile,
+ * permission will be denied.
+ *
+ * @see GuacamoleTunnelService#getGuacamoleTunnel(RemoteAuthenticatedUser,
+ * SharedConnectionDefinition, GuacamoleClientInformation, Map)
+ *
+ * @see #getSharingCredentials(SharedConnectionDefinition)
*
* @param user
* The user sharing the connection.
@@ -88,42 +98,67 @@
*
* @param sharingProfileIdentifier
* The identifier of the sharing profile dictating the semantics or
- * restrictions applying to the shared session.
+ * restrictions applying to the shared session, or null if no such
+ * restrictions should apply.
*
* @return
- * A newly-generated set of temporary credentials which can be used to
- * connect to the given connection.
+ * A new SharedConnectionDefinition which can be used to connect to the
+ * given connection.
*
* @throws GuacamoleException
* If permission to share the given connection is denied.
*/
- public UserCredentials generateTemporaryCredentials(ModeledAuthenticatedUser user,
+ public SharedConnectionDefinition shareConnection(ModeledAuthenticatedUser user,
ActiveConnectionRecord activeConnection,
String sharingProfileIdentifier) throws GuacamoleException {
- // Pull sharing profile (verifying access)
- ModeledSharingProfile sharingProfile =
- sharingProfileService.retrieveObject(user,
- sharingProfileIdentifier);
+ // If a sharing profile is provided, verify that permission to use that
+ // profile to share the given connection is actually granted
+ ModeledSharingProfile sharingProfile = null;
+ if (sharingProfileIdentifier != null) {
- // Verify that this profile is indeed a sharing profile for the
- // requested connection
- String connectionIdentifier = activeConnection.getConnectionIdentifier();
- if (sharingProfile == null || !sharingProfile.getPrimaryConnectionIdentifier().equals(connectionIdentifier))
- throw new GuacamoleSecurityException("Permission denied.");
+ // Pull sharing profile (verifying access)
+ sharingProfile = sharingProfileService.retrieveObject(user, sharingProfileIdentifier);
+
+ // Verify that this profile is indeed a sharing profile for the
+ // requested connection
+ String connectionIdentifier = activeConnection.getConnectionIdentifier();
+ if (sharingProfile == null || !sharingProfile.getPrimaryConnectionIdentifier().equals(connectionIdentifier))
+ throw new GuacamoleSecurityException("Permission denied.");
+
+ }
// Generate a share key for the requested connection
String key = keyGenerator.getShareKey();
- connectionMap.add(new SharedConnectionDefinition(activeConnection,
- sharingProfile, key));
+ SharedConnectionDefinition definition = new SharedConnectionDefinition(activeConnection, sharingProfile, key);
+ connectionMap.add(definition);
// Ensure the share key is properly invalidated when the original
// connection is closed
activeConnection.registerShareKey(key);
+ return definition;
+
+ }
+
+ /**
+ * Generates a set of temporary credentials which can be used to connect to
+ * the given connection shared by the SharedConnectionDefinition.
+ *
+ * @param definition
+ * The SharedConnectionDefinition which defines the connection being
+ * shared and any applicable restrictions.
+ *
+ * @return
+ * A newly-generated set of temporary credentials which can be used to
+ * connect to the connection shared by the given
+ * SharedConnectionDefinition.
+ */
+ public UserCredentials getSharingCredentials(SharedConnectionDefinition definition) {
+
// Return credentials defining a single expected parameter
return new UserCredentials(SHARE_KEY,
- Collections.singletonMap(SHARE_KEY_NAME, key));
+ Collections.singletonMap(SHARE_KEY_NAME, definition.getShareKey()));
}
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnectionDefinition.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnectionDefinition.java
index cb48013..4e7c3d5 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnectionDefinition.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnectionDefinition.java
@@ -29,9 +29,11 @@
/**
* Defines the semantics/restrictions of a shared connection by associating an
- * active connection with a sharing profile. The sharing profile defines the
- * access provided to users of the shared active connection through its
- * connection parameters.
+ * active connection with an optional sharing profile. The sharing profile, if
+ * present, defines the access provided to users of the shared active
+ * connection through its connection parameters. If no sharing profile is
+ * present, the shared connection has the same level of access as the original
+ * connection.
*/
public class SharedConnectionDefinition {
@@ -88,7 +90,8 @@
*
* @param sharingProfile
* A sharing profile whose associated parameters dictate the level of
- * access provided to the shared connection.
+ * access provided to the shared connection, or null if the connection
+ * should be given full access.
*
* @param shareKey
* The unique key with which a user may access the shared connection.
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java
index 20ac299..abecf32 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/AbstractGuacamoleTunnelService.java
@@ -202,8 +202,10 @@
/**
* Returns a guacamole configuration containing the protocol and parameters
- * from the given connection. If tokens are used in the connection
- * parameter values, credentials from the given user will be substituted
+ * from the given connection. If the ID of an active connection is
+ * provided, that connection will be joined instead of starting a new
+ * primary connection. If tokens are used in the connection parameter
+ * values, credentials from the given user will be substituted
* appropriately.
*
* @param user
@@ -213,19 +215,29 @@
* The connection whose protocol and parameters should be added to the
* returned configuration.
*
+ * @param connectionID
+ * The ID of the active connection to be joined, as returned by guacd,
+ * or null if a new primary connection should be established.
+ *
* @return
* A GuacamoleConfiguration containing the protocol and parameters from
* the given connection.
*/
private GuacamoleConfiguration getGuacamoleConfiguration(RemoteAuthenticatedUser user,
- ModeledConnection connection) {
+ ModeledConnection connection, String connectionID) {
// Generate configuration from available data
GuacamoleConfiguration config = new GuacamoleConfiguration();
- // Set protocol from connection
- ConnectionModel model = connection.getModel();
- config.setProtocol(model.getProtocol());
+ // Join existing active connection, if any
+ if (connectionID != null)
+ config.setConnectionID(connectionID);
+
+ // Set protocol from connection if not joining an active connection
+ else {
+ ConnectionModel model = connection.getModel();
+ config.setProtocol(model.getProtocol());
+ }
// Set parameters from associated data
Collection<ConnectionParameterModel> parameters = connectionParameterMapper.select(connection.getIdentifier());
@@ -470,16 +482,17 @@
// Retrieve connection information associated with given connection record
ModeledConnection connection = activeConnection.getConnection();
- // Pull configuration directly from the connection if we are not
- // joining an active connection
+ // Pull configuration directly from the connection, additionally
+ // joining the existing active connection (without sharing profile
+ // restrictions) if such a connection exists
if (activeConnection.isPrimaryConnection()) {
activeConnections.put(connection.getIdentifier(), activeConnection);
activeConnectionGroups.put(connection.getParentIdentifier(), activeConnection);
- config = getGuacamoleConfiguration(activeConnection.getUser(), connection);
+ config = getGuacamoleConfiguration(activeConnection.getUser(), connection, activeConnection.getConnectionID());
}
- // If we ARE joining an active connection, generate a configuration
- // which does so
+ // If we ARE joining an active connection under the restrictions of
+ // a sharing profile, generate a configuration which does so
else {
// Verify that the connection ID is known
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java
index 3a4b148..a150212 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/ActiveConnectionRecord.java
@@ -32,7 +32,6 @@
import org.apache.guacamole.net.GuacamoleSocket;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.ConnectionRecord;
-import org.apache.guacamole.protocol.ConfiguredGuacamoleSocket;
/**
@@ -202,8 +201,8 @@
*
* @param sharingProfile
* The sharing profile that was used to share access to the given
- * connection. As a record created in this way always refers to a
- * shared connection, this value may NOT be null.
+ * connection, or null if no sharing profile should be used (access to
+ * the connection is unrestricted).
*/
public void init(RemoteAuthenticatedUser user,
ActiveConnectionRecord activeConnection,
diff --git a/extensions/guacamole-auth-ldap/pom.xml b/extensions/guacamole-auth-ldap/pom.xml
index 898deaf..17c8ed6 100644
--- a/extensions/guacamole-auth-ldap/pom.xml
+++ b/extensions/guacamole-auth-ldap/pom.xml
@@ -141,11 +141,17 @@
<scope>provided</scope>
</dependency>
- <!-- JLDAP -->
+ <!-- Apache Directory LDAP API -->
<dependency>
- <groupId>com.novell.ldap</groupId>
- <artifactId>jldap</artifactId>
- <version>4.3</version>
+ <groupId>org.apache.directory.api</groupId>
+ <artifactId>api-all</artifactId>
+ <version>2.0.0.AM4</version>
+ <exclusions>
+ <exclusion>
+ <groupId>org.slf4j</groupId>
+ <artifactId>slf4j-api</artifactId>
+ </exclusion>
+ </exclusions>
</dependency>
<!-- Guice -->
diff --git a/extensions/guacamole-auth-ldap/src/licenses/LICENSE b/extensions/guacamole-auth-ldap/src/licenses/LICENSE
index 9368e8f..0414ad6 100644
--- a/extensions/guacamole-auth-ldap/src/licenses/LICENSE
+++ b/extensions/guacamole-auth-ldap/src/licenses/LICENSE
@@ -220,6 +220,15 @@
Public Domain (bundled/aopalliance-1.0/LICENSE)
+Apache Directory LDAP API (http://directory.apache.org)
+-------------------------------------------------------
+
+ Version: 2.0.0.AM4
+ From: 'Apache Software Foundation' (http://apache.org)
+ License(s):
+ Apache v2.0 (bundled/directory-api-2.0.0/LICENSE-2.0.txt)
+
+
Google Guice (https://github.com/google/guice)
----------------------------------------------
@@ -229,20 +238,6 @@
Apache v2.0 (bundled/guice-3.0/COPYING)
-JLDAP (http://www.openldap.org/jldap/)
---------------------------------------
-
- Version: 4.3
- From: 'The OpenLDAP Foundation' (http://www.openldap.org/)
- License(s):
- OpenLDAP Public License v2.8 (bundled/jldap-4.3/LICENSE)
- OpenLDAP Public License v2.0.1 (bundled/jldap-4.3/LICENSE-2.0.1)
-
-NOTE: JLDAP is *NOT* dual-licensed. Whether a particular source file is under
-version 2.8 or 2.0.1 of the OpenLDAP Public License depends on the license
-header of the file in question.
-
-
JSR-330 / Dependency Injection for Java (http://code.google.com/p/atinject/)
----------------------------------------------------------------------------
diff --git a/extensions/guacamole-auth-ldap/src/licenses/bundled/directory-api-2.0.0/LICENSE-2.0.txt b/extensions/guacamole-auth-ldap/src/licenses/bundled/directory-api-2.0.0/LICENSE-2.0.txt
new file mode 100644
index 0000000..d645695
--- /dev/null
+++ b/extensions/guacamole-auth-ldap/src/licenses/bundled/directory-api-2.0.0/LICENSE-2.0.txt
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed 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.
diff --git a/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE b/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE
deleted file mode 100644
index 05ad757..0000000
--- a/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE
+++ /dev/null
@@ -1,47 +0,0 @@
-The OpenLDAP Public License
- Version 2.8, 17 August 2003
-
-Redistribution and use of this software and associated documentation
-("Software"), with or without modification, are permitted provided
-that the following conditions are met:
-
-1. Redistributions in source form must retain copyright statements
- and notices,
-
-2. Redistributions in binary form must reproduce applicable copyright
- statements and notices, this list of conditions, and the following
- disclaimer in the documentation and/or other materials provided
- with the distribution, and
-
-3. Redistributions must contain a verbatim copy of this document.
-
-The OpenLDAP Foundation may revise this license from time to time.
-Each revision is distinguished by a version number. You may use
-this Software under terms of this license revision or under the
-terms of any subsequent revision of the license.
-
-THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND ITS
-CONTRIBUTORS ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
-INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
-AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
-SHALL THE OPENLDAP FOUNDATION, ITS CONTRIBUTORS, OR THE AUTHOR(S)
-OR OWNER(S) OF THE SOFTWARE BE LIABLE FOR ANY DIRECT, INDIRECT,
-INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
-BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
-LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
-ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGE.
-
-The names of the authors and copyright holders must not be used in
-advertising or otherwise to promote the sale, use or other dealing
-in this Software without specific, written prior permission. Title
-to copyright in this Software shall at all times remain with copyright
-holders.
-
-OpenLDAP is a registered trademark of the OpenLDAP Foundation.
-
-Copyright 1999-2003 The OpenLDAP Foundation, Redwood City,
-California, USA. All Rights Reserved. Permission to copy and
-distribute verbatim copies of this document is granted.
diff --git a/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE-2.0.1 b/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE-2.0.1
deleted file mode 100644
index db93ec5..0000000
--- a/extensions/guacamole-auth-ldap/src/licenses/bundled/jldap-4.3/LICENSE-2.0.1
+++ /dev/null
@@ -1,56 +0,0 @@
-A number of files contained in OpenLDAP Software contain
-a statement:
- USE, MODIFICATION, AND REDISTRIBUTION OF THIS WORK IS SUBJECT
- TO VERSION 2.0.1 OF THE OPENLDAP PUBLIC LICENSE, A COPY OF
- WHICH IS AVAILABLE AT HTTP://WWW.OPENLDAP.ORG/LICENSE.HTML OR
- IN THE FILE "LICENSE" IN THE TOP-LEVEL DIRECTORY OF THE
- DISTRIBUTION.
-
-The following is a verbatim copy of version 2.0.1 of the OpenLDAP
-Public License referenced in the above statement.
-
-
-The OpenLDAP Public License
-
- Version 2.0.1, 21 December 1999
- Copyright 1999, The OpenLDAP Foundation, Redwood City, California, USA.
- All Rights Reserved.
-
-Redistribution and use of this software and associated documentation
-("Software"), with or without modification, are permitted provided
-that the following conditions are met:
-
-1. Redistributions of source code must retain copyright
-statements and notices. Redistributions must also contain a
-copy of this document.
-
-2. Redistributions in binary form must reproduce the
-above copyright notice, this list of conditions and the
-following disclaimer in the documentation and/or other
-materials provided with the distribution.
-
-3. The name "OpenLDAP" must not be used to endorse or promote
-products derived from this Software without prior written
-permission of the OpenLDAP Foundation. For written permission,
-please contact foundation@openldap.org.
-
-4. Products derived from this Software may not be called "OpenLDAP"
-nor may "OpenLDAP" appear in their names without prior written
-permission of the OpenLDAP Foundation. OpenLDAP is a trademark
-of the OpenLDAP Foundation.
-
-5. Due credit should be given to the OpenLDAP Project
-(http://www.openldap.org/).
-
-THIS SOFTWARE IS PROVIDED BY THE OPENLDAP FOUNDATION AND CONTRIBUTORS
-``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
-NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
-FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
-THE OPENLDAP FOUNDATION OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
-INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
-HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
-STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
-ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
-OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java
index 949d1c8..9004c13 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/AuthenticationProviderService.java
@@ -21,27 +21,29 @@
import com.google.inject.Inject;
import com.google.inject.Provider;
-import com.novell.ldap.LDAPAttribute;
-import com.novell.ldap.LDAPAttributeSet;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
-import com.novell.ldap.LDAPException;
+import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
import org.apache.guacamole.auth.ldap.group.UserGroupService;
import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser;
import org.apache.guacamole.auth.ldap.user.LDAPUserContext;
import org.apache.guacamole.auth.ldap.user.UserService;
import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.Credentials;
+import org.apache.guacamole.token.TokenName;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
-import org.apache.guacamole.token.TokenName;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -113,16 +115,15 @@
* If required properties are missing, and thus the user DN cannot be
* determined.
*/
- private String getUserBindDN(String username)
- throws GuacamoleException {
+ private Dn getUserBindDN(String username) throws GuacamoleException {
// If a search DN is provided, search the LDAP directory for the DN
// corresponding to the given username
- String searchBindDN = confService.getSearchBindDN();
+ Dn searchBindDN = confService.getSearchBindDN();
if (searchBindDN != null) {
// Create an LDAP connection using the search account
- LDAPConnection searchConnection = ldapService.bindAs(
+ LdapNetworkConnection searchConnection = ldapService.bindAs(
searchBindDN,
confService.getSearchBindPassword()
);
@@ -136,7 +137,7 @@
try {
// Retrieve all DNs associated with the given username
- List<String> userDNs = userService.getUserDNs(searchConnection, username);
+ List<Dn> userDNs = userService.getUserDNs(searchConnection, username);
if (userDNs.isEmpty())
return null;
@@ -164,53 +165,6 @@
}
/**
- * Binds to the LDAP server using the provided Guacamole credentials. The
- * DN of the user is derived using the LDAP configuration properties
- * provided in guacamole.properties, as is the server hostname and port
- * information.
- *
- * @param credentials
- * The credentials to use to bind to the LDAP server.
- *
- * @return
- * A bound LDAP connection, or null if the connection could not be
- * bound.
- *
- * @throws GuacamoleException
- * If an error occurs while binding to the LDAP server.
- */
- private LDAPConnection bindAs(Credentials credentials)
- throws GuacamoleException {
-
- // Get username and password from credentials
- String username = credentials.getUsername();
- String password = credentials.getPassword();
-
- // Require username
- if (username == null || username.isEmpty()) {
- logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
- return null;
- }
-
- // Require password, and do not allow anonymous binding
- if (password == null || password.isEmpty()) {
- logger.debug("Anonymous bind is not currently allowed by the LDAP authentication provider.");
- return null;
- }
-
- // Determine user DN
- String userDN = getUserBindDN(username);
- if (userDN == null) {
- logger.debug("Unable to determine DN for user \"{}\".", username);
- return null;
- }
-
- // Bind using user's DN
- return ldapService.bindAs(userDN, password);
-
- }
-
- /**
* Returns an AuthenticatedUser representing the user authenticated by the
* given credentials. Also adds custom LDAP attributes to the
* AuthenticatedUser.
@@ -228,39 +182,40 @@
*/
public LDAPAuthenticatedUser authenticateUser(Credentials credentials)
throws GuacamoleException {
-
+
+ String username = credentials.getUsername();
+ String password = credentials.getPassword();
+
+ // Username and password are required
+ if (username == null
+ || username.isEmpty()
+ || password == null
+ || password.isEmpty()) {
+ throw new GuacamoleInvalidCredentialsException(
+ "Anonymous bind is not currently allowed by the LDAP"
+ + " authentication provider.", CredentialsInfo.USERNAME_PASSWORD);
+ }
+
+ Dn bindDn = getUserBindDN(username);
+ if (bindDn == null || bindDn.isEmpty()) {
+ throw new GuacamoleInvalidCredentialsException("Unable to determine"
+ + " DN of user " + username, CredentialsInfo.USERNAME_PASSWORD);
+ }
+
// Attempt bind
- LDAPConnection ldapConnection;
- try {
- ldapConnection = bindAs(credentials);
- }
- catch (GuacamoleException e) {
- logger.error("Cannot bind with LDAP server: {}", e.getMessage());
- logger.debug("Error binding with LDAP server.", e);
- ldapConnection = null;
- }
+ LdapNetworkConnection ldapConnection = ldapService.bindAs(bindDn, password);
+
+ // Retrieve group membership of the user that just authenticated
+ Set<String> effectiveGroups =
+ userGroupService.getParentUserGroupIdentifiers(ldapConnection,
+ bindDn);
- // If bind fails, permission to login is denied
- if (ldapConnection == null)
- throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
-
- try {
-
- // Retrieve group membership of the user that just authenticated
- Set<String> effectiveGroups =
- userGroupService.getParentUserGroupIdentifiers(ldapConnection,
- ldapConnection.getAuthenticationDN());
-
- // Return AuthenticatedUser if bind succeeds
- LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
- authenticatedUser.init(credentials, getAttributeTokens(ldapConnection, credentials.getUsername()), effectiveGroups);
- return authenticatedUser;
-
- }
- // Always disconnect
- finally {
- ldapService.disconnect(ldapConnection);
- }
+ // Return AuthenticatedUser if bind succeeds
+ LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
+ authenticatedUser.init(credentials, getAttributeTokens(ldapConnection,
+ bindDn), effectiveGroups, bindDn);
+
+ return authenticatedUser;
}
@@ -286,8 +241,8 @@
* @throws GuacamoleException
* If an error occurs retrieving the user DN or the attributes.
*/
- private Map<String, String> getAttributeTokens(LDAPConnection ldapConnection,
- String username) throws GuacamoleException {
+ private Map<String, String> getAttributeTokens(LdapNetworkConnection ldapConnection,
+ Dn userDn) throws GuacamoleException {
// Get attributes from configuration information
List<String> attrList = confService.getAttributes();
@@ -298,29 +253,27 @@
// Build LDAP query parameters
String[] attrArray = attrList.toArray(new String[attrList.size()]);
- String userDN = getUserBindDN(username);
Map<String, String> tokens = new HashMap<>();
try {
// Get LDAP attributes by querying LDAP
- LDAPEntry userEntry = ldapConnection.read(userDN, attrArray);
+ Entry userEntry = ldapConnection.lookup(userDn, attrArray);
if (userEntry == null)
return Collections.<String, String>emptyMap();
- LDAPAttributeSet attrSet = userEntry.getAttributeSet();
- if (attrSet == null)
+ Collection<Attribute> attributes = userEntry.getAttributes();
+ if (attributes == null)
return Collections.<String, String>emptyMap();
// Convert each retrieved attribute into a corresponding token
- for (Object attrObj : attrSet) {
- LDAPAttribute attr = (LDAPAttribute)attrObj;
- tokens.put(TokenName.canonicalize(attr.getName(),
- LDAP_ATTRIBUTE_TOKEN_PREFIX), attr.getStringValue());
+ for (Attribute attr : attributes) {
+ tokens.put(TokenName.canonicalize(attr.getId(),
+ LDAP_ATTRIBUTE_TOKEN_PREFIX), attr.getString());
}
}
- catch (LDAPException e) {
+ catch (LdapException e) {
throw new GuacamoleServerException("Could not query LDAP user attributes.", e);
}
@@ -347,23 +300,25 @@
// Bind using credentials associated with AuthenticatedUser
Credentials credentials = authenticatedUser.getCredentials();
- LDAPConnection ldapConnection = bindAs(credentials);
- if (ldapConnection == null)
- return null;
+ if (authenticatedUser instanceof LDAPAuthenticatedUser) {
+ Dn bindDn = ((LDAPAuthenticatedUser) authenticatedUser).getBindDn();
+ LdapNetworkConnection ldapConnection = ldapService.bindAs(bindDn, credentials.getPassword());
- try {
+ try {
- // Build user context by querying LDAP
- LDAPUserContext userContext = userContextProvider.get();
- userContext.init(authenticatedUser, ldapConnection);
- return userContext;
+ // Build user context by querying LDAP
+ LDAPUserContext userContext = userContextProvider.get();
+ userContext.init(authenticatedUser, ldapConnection);
+ return userContext;
+ }
+
+ // Always disconnect
+ finally {
+ ldapService.disconnect(ldapConnection);
+ }
}
-
- // Always disconnect
- finally {
- ldapService.disconnect(ldapConnection);
- }
+ return null;
}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesMode.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesMode.java
deleted file mode 100644
index 1fd1bea..0000000
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesMode.java
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * 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.ldap;
-
-import com.novell.ldap.LDAPSearchConstraints;
-
-/**
- * Data type that handles acceptable values for configuring
- * alias dereferencing behavior when querying LDAP servers.
- */
-public enum DereferenceAliasesMode {
-
- /**
- * Never dereference aliases. This is the default.
- */
- NEVER(LDAPSearchConstraints.DEREF_NEVER),
-
- /**
- * Aliases are dereferenced below the base object, but not to locate
- * the base object itself. So, if the base object is itself an alias
- * the search will not complete.
- */
- SEARCHING(LDAPSearchConstraints.DEREF_SEARCHING),
-
- /**
- * Aliases are only dereferenced to locate the base object, but not
- * after that. So, a search against a base object that is an alias will
- * find any subordinates of the real object the alias references, but
- * further aliases in the search will not be dereferenced.
- */
- FINDING(LDAPSearchConstraints.DEREF_FINDING),
-
- /**
- * Aliases will always be dereferenced, both to locate the base object
- * and when handling results returned by the search.
- */
- ALWAYS(LDAPSearchConstraints.DEREF_ALWAYS);
-
- /**
- * The integer constant as defined in the JLDAP library that
- * the LDAPSearchConstraints class uses to define the
- * dereferencing behavior during search operations.
- */
- public final int DEREF_VALUE;
-
- /**
- * Initializes the dereference aliases object with the integer
- * value the setting maps to per the JLDAP implementation.
- *
- * @param derefValue
- * The value associated with this dereference setting
- */
- private DereferenceAliasesMode(int derefValue) {
- this.DEREF_VALUE = derefValue;
- }
-
-}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EscapingService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EscapingService.java
deleted file mode 100644
index 5dce244..0000000
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EscapingService.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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.ldap;
-
-/**
- * Service for escaping LDAP filters, distinguished names (DN's), etc.
- */
-public class EscapingService {
-
- /**
- * Escapes the given string for use within an LDAP search filter. This
- * implementation is provided courtesy of OWASP:
- *
- * https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
- *
- * @param filter
- * The string to escape such that it has no special meaning within an
- * LDAP search filter.
- *
- * @return
- * The escaped string, safe for use within an LDAP search filter.
- */
- public String escapeLDAPSearchFilter(String filter) {
- StringBuilder sb = new StringBuilder();
- for (int i = 0; i < filter.length(); i++) {
- char curChar = filter.charAt(i);
- switch (curChar) {
- case '\\':
- sb.append("\\5c");
- break;
- case '*':
- sb.append("\\2a");
- break;
- case '(':
- sb.append("\\28");
- break;
- case ')':
- sb.append("\\29");
- break;
- case '\u0000':
- sb.append("\\00");
- break;
- default:
- sb.append(curChar);
- }
- }
- return sb.toString();
- }
-
- /**
- * Escapes the given string such that it is safe for use within an LDAP
- * distinguished name (DN). This implementation is provided courtesy of
- * OWASP:
- *
- * https://www.owasp.org/index.php/Preventing_LDAP_Injection_in_Java
- *
- * @param name
- * The string to escape such that it has no special meaning within an
- * LDAP DN.
- *
- * @return
- * The escaped string, safe for use within an LDAP DN.
- */
- public String escapeDN(String name) {
- StringBuilder sb = new StringBuilder();
- if ((name.length() > 0) && ((name.charAt(0) == ' ') || (name.charAt(0) == '#'))) {
- sb.append('\\'); // add the leading backslash if needed
- }
- for (int i = 0; i < name.length(); i++) {
- char curChar = name.charAt(i);
- switch (curChar) {
- case '\\':
- sb.append("\\\\");
- break;
- case ',':
- sb.append("\\,");
- break;
- case '+':
- sb.append("\\+");
- break;
- case '"':
- sb.append("\\\"");
- break;
- case '<':
- sb.append("\\<");
- break;
- case '>':
- sb.append("\\>");
- break;
- case ';':
- sb.append("\\;");
- break;
- default:
- sb.append(curChar);
- }
- }
- if ((name.length() > 1) && (name.charAt(name.length() - 1) == ' ')) {
- sb.insert(sb.length() - 1, '\\'); // add the trailing backslash if needed
- }
- return sb.toString();
- }
-
-}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java
index 23decec..9cfaadf 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProviderModule.java
@@ -20,6 +20,7 @@
package org.apache.guacamole.auth.ldap;
import com.google.inject.AbstractModule;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
import org.apache.guacamole.auth.ldap.connection.ConnectionService;
import org.apache.guacamole.auth.ldap.user.UserService;
import org.apache.guacamole.GuacamoleException;
@@ -76,7 +77,6 @@
// Bind LDAP-specific services
bind(ConfigurationService.class);
bind(ConnectionService.class);
- bind(EscapingService.class);
bind(LDAPConnectionService.class);
bind(ObjectQueryService.class);
bind(UserGroupService.class);
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java
index 3aaf324..49a3f7c 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java
@@ -20,14 +20,28 @@
package org.apache.guacamole.auth.ldap;
import com.google.inject.Inject;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPConstraints;
-import com.novell.ldap.LDAPException;
-import com.novell.ldap.LDAPJSSESecureSocketFactory;
-import com.novell.ldap.LDAPJSSEStartTLSFactory;
-import java.io.UnsupportedEncodingException;
+import java.io.IOException;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.message.BindRequest;
+import org.apache.directory.api.ldap.model.message.BindRequestImpl;
+import org.apache.directory.api.ldap.model.message.BindResponse;
+import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
+import org.apache.directory.api.ldap.model.message.SearchRequest;
+import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
+import org.apache.directory.api.ldap.model.message.SearchScope;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.url.LdapUrl;
+import org.apache.directory.ldap.client.api.LdapConnection;
+import org.apache.directory.ldap.client.api.LdapConnectionConfig;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.GuacamoleUnsupportedException;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
+import org.apache.guacamole.auth.ldap.conf.EncryptionMethod;
+import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
+import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -39,7 +53,7 @@
/**
* Logger for this class.
*/
- private final Logger logger = LoggerFactory.getLogger(LDAPConnectionService.class);
+ private static final Logger logger = LoggerFactory.getLogger(LDAPConnectionService.class);
/**
* Service for retrieving LDAP server configuration information.
@@ -48,19 +62,24 @@
private ConfigurationService confService;
/**
- * Creates a new instance of LDAPConnection, configured as required to use
- * whichever encryption method is requested within guacamole.properties.
+ * Creates a new instance of LdapNetworkConnection, configured as required
+ * to use whichever encryption method is requested within
+ * guacamole.properties.
*
* @return
- * A new LDAPConnection instance which has already been configured to
- * use the encryption method requested within guacamole.properties.
+ * A new LdapNetworkConnection instance which has already been
+ * configured to use the encryption method requested within
+ * guacamole.properties.
*
* @throws GuacamoleException
* If an error occurs while parsing guacamole.properties, or if the
* requested encryption method is actually not implemented (a bug).
*/
- private LDAPConnection createLDAPConnection() throws GuacamoleException {
+ private LdapNetworkConnection createLDAPConnection() throws GuacamoleException {
+ String host = confService.getServerHostname();
+ int port = confService.getServerPort();
+
// Map encryption method to proper connection and socket factory
EncryptionMethod encryptionMethod = confService.getEncryptionMethod();
switch (encryptionMethod) {
@@ -68,17 +87,17 @@
// Unencrypted LDAP connection
case NONE:
logger.debug("Connection to LDAP server without encryption.");
- return new LDAPConnection();
+ return new LdapNetworkConnection(host, port);
// LDAP over SSL (LDAPS)
case SSL:
logger.debug("Connecting to LDAP server using SSL/TLS.");
- return new LDAPConnection(new LDAPJSSESecureSocketFactory());
+ return new LdapNetworkConnection(host, port, true);
// LDAP + STARTTLS
case STARTTLS:
logger.debug("Connecting to LDAP server using STARTTLS.");
- return new LDAPConnection(new LDAPJSSEStartTLSFactory());
+ return new LdapNetworkConnection(host, port);
// The encryption method, though known, is not actually
// implemented. If encountered, this would be a bug.
@@ -106,86 +125,103 @@
* @throws GuacamoleException
* If an error occurs while binding to the LDAP server.
*/
- public LDAPConnection bindAs(String userDN, String password)
+ public LdapNetworkConnection bindAs(Dn userDN, String password)
throws GuacamoleException {
- // Obtain appropriately-configured LDAPConnection instance
- LDAPConnection ldapConnection = createLDAPConnection();
-
- // Configure LDAP connection constraints
- LDAPConstraints ldapConstraints = ldapConnection.getConstraints();
- if (ldapConstraints == null)
- ldapConstraints = new LDAPConstraints();
-
- // Set whether or not we follow referrals
- ldapConstraints.setReferralFollowing(confService.getFollowReferrals());
-
- // Set referral authentication to use the provided credentials.
- if (userDN != null && !userDN.isEmpty())
- ldapConstraints.setReferralHandler(new ReferralAuthHandler(userDN, password));
-
- // Set the maximum number of referrals we follow
- ldapConstraints.setHopLimit(confService.getMaxReferralHops());
-
- // Set timelimit to wait for LDAP operations, converting to ms
- ldapConstraints.setTimeLimit(confService.getOperationTimeout() * 1000);
-
- // Apply the constraints to the connection
- ldapConnection.setConstraints(ldapConstraints);
-
- try {
+ // Get ldapConnection and try to connect and bind.
+ try (LdapNetworkConnection ldapConnection = createLDAPConnection()) {
// Connect to LDAP server
- ldapConnection.connect(
- confService.getServerHostname(),
- confService.getServerPort()
- );
+ ldapConnection.connect();
// Explicitly start TLS if requested
if (confService.getEncryptionMethod() == EncryptionMethod.STARTTLS)
- ldapConnection.startTLS();
+ ldapConnection.startTls();
- }
- catch (LDAPException e) {
- logger.error("Unable to connect to LDAP server: {}", e.getMessage());
- logger.debug("Failed to connect to LDAP server.", e);
- return null;
- }
-
- // Bind using provided credentials
- try {
-
- byte[] passwordBytes;
- try {
-
- // Convert password into corresponding byte array
- if (password != null)
- passwordBytes = password.getBytes("UTF-8");
- else
- passwordBytes = null;
-
- }
- catch (UnsupportedEncodingException e) {
- logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage());
- logger.debug("Support for UTF-8 (as required by Java spec) not found.", e);
- disconnect(ldapConnection);
- return null;
- }
-
- // Bind as user
- ldapConnection.bind(LDAPConnection.LDAP_V3, userDN, passwordBytes);
+ // Bind using provided credentials
+ BindRequest bindRequest = new BindRequestImpl();
+ bindRequest.setDn(userDN);
+ bindRequest.setCredentials(password);
+ BindResponse bindResponse = ldapConnection.bind(bindRequest);
+ if (bindResponse.getLdapResult().getResultCode() == ResultCodeEnum.SUCCESS)
+ return ldapConnection;
+
+ else
+ throw new GuacamoleInvalidCredentialsException("Error binding"
+ + " to server: " + bindResponse.toString(),
+ CredentialsInfo.USERNAME_PASSWORD);
}
// Disconnect if an error occurs during bind
- catch (LDAPException e) {
- logger.debug("LDAP bind failed.", e);
- disconnect(ldapConnection);
- return null;
+ catch (LdapException e) {
+ logger.debug("Unable to bind to LDAP server.", e);
+ throw new GuacamoleInvalidCredentialsException(
+ "Unable to bind to the LDAP server.",
+ CredentialsInfo.USERNAME_PASSWORD);
}
- return ldapConnection;
-
+ }
+
+ /**
+ * Generate a new LdapNetworkConnection object for following a referral
+ * with the given LdapUrl, and copy the username and password
+ * from the original connection.
+ *
+ * @param referralUrl
+ * The LDAP URL to follow.
+ *
+ * @param ldapConfig
+ * The connection configuration to use to retrieve username and
+ * password.
+ *
+ * @param hop
+ * The current hop number of this referral - once the configured
+ * limit is reached, this method will throw an exception.
+ *
+ * @return
+ * A LdapNetworkConnection object that points at the location
+ * specified in the referralUrl.
+ *
+ * @throws GuacamoleException
+ * If an error occurs parsing out the LdapUrl object or the
+ * maximum number of referral hops is reached.
+ */
+ public LdapNetworkConnection getReferralConnection(LdapUrl referralUrl,
+ LdapConnectionConfig ldapConfig, int hop)
+ throws GuacamoleException {
+
+ if (hop >= confService.getMaxReferralHops())
+ throw new GuacamoleServerException("Maximum number of referrals reached.");
+
+ LdapConnectionConfig referralConfig = new LdapConnectionConfig();
+
+ // Copy bind name and password from original config
+ referralConfig.setName(ldapConfig.getName());
+ referralConfig.setCredentials(ldapConfig.getCredentials());
+
+ // Look for host - if not there, bail out.
+ String host = referralUrl.getHost();
+ if (host == null || host.isEmpty())
+ throw new GuacamoleServerException("Referral URL contains no host.");
+
+ referralConfig.setLdapHost(host);
+
+ // Look for port, or assign a default.
+ int port = referralUrl.getPort();
+ if (port < 1)
+ referralConfig.setLdapPort(389);
+ else
+ referralConfig.setLdapPort(port);
+
+ // Deal with SSL connections
+ if (referralUrl.getScheme().equals(LdapUrl.LDAPS_SCHEME))
+ referralConfig.setUseSsl(true);
+ else
+ referralConfig.setUseSsl(false);
+
+ return new LdapNetworkConnection(referralConfig);
+
}
/**
@@ -195,19 +231,53 @@
* @param ldapConnection
* The LDAP connection to disconnect.
*/
- public void disconnect(LDAPConnection ldapConnection) {
+ public void disconnect(LdapConnection ldapConnection) {
// Attempt disconnect
try {
- ldapConnection.disconnect();
+ ldapConnection.close();
}
// Warn if disconnect unexpectedly fails
- catch (LDAPException e) {
+ catch (IOException e) {
logger.warn("Unable to disconnect from LDAP server: {}", e.getMessage());
logger.debug("LDAP disconnect failed.", e);
}
}
+
+ /**
+ * Generate a SearchRequest object using the given Base DN and filter
+ * and retrieving other properties from the LDAP configuration service.
+ *
+ * @param baseDn
+ * The LDAP Base DN at which to search the search.
+ *
+ * @param filter
+ * A string representation of a LDAP filter to use for the search.
+ *
+ * @return
+ * The properly-configured SearchRequest object.
+ *
+ * @throws GuacamoleException
+ * If an error occurs retrieving any of the configuration values.
+ */
+ public SearchRequest getSearchRequest(Dn baseDn, ExprNode filter)
+ throws GuacamoleException {
+
+ SearchRequest searchRequest = new SearchRequestImpl();
+ searchRequest.setBase(baseDn);
+ searchRequest.setDerefAliases(confService.getDereferenceAliases());
+ searchRequest.setScope(SearchScope.SUBTREE);
+ searchRequest.setFilter(filter);
+ searchRequest.setSizeLimit(confService.getMaxResults());
+ searchRequest.setTimeLimit(confService.getOperationTimeout());
+ searchRequest.setTypesOnly(false);
+
+ if (confService.getFollowReferrals())
+ searchRequest.followReferrals();
+
+ return searchRequest;
+ }
}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
index 2196c2f..ebf9792 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
@@ -20,18 +20,29 @@
package org.apache.guacamole.auth.ldap;
import com.google.inject.Inject;
-import com.novell.ldap.LDAPAttribute;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
-import com.novell.ldap.LDAPException;
-import com.novell.ldap.LDAPReferralException;
-import com.novell.ldap.LDAPSearchResults;
+import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
+import org.apache.directory.api.ldap.model.cursor.CursorException;
+import org.apache.directory.api.ldap.model.cursor.SearchCursor;
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
+import org.apache.directory.api.ldap.model.filter.AndNode;
+import org.apache.directory.api.ldap.model.filter.EqualityNode;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.OrNode;
+import org.apache.directory.api.ldap.model.message.Referral;
+import org.apache.directory.api.ldap.model.message.SearchRequest;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.url.LdapUrl;
+import org.apache.directory.ldap.client.api.LdapConnectionConfig;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.net.auth.Identifiable;
@@ -50,19 +61,13 @@
/**
* Logger for this class.
*/
- private final Logger logger = LoggerFactory.getLogger(ObjectQueryService.class);
-
+ private static final Logger logger = LoggerFactory.getLogger(ObjectQueryService.class);
+
/**
- * Service for escaping parts of LDAP queries.
+ * Service for connecting to LDAP directory.
*/
@Inject
- private EscapingService escapingService;
-
- /**
- * Service for retrieving LDAP server configuration information.
- */
- @Inject
- private ConfigurationService confService;
+ private LDAPConnectionService ldapService;
/**
* Returns the identifier of the object represented by the given LDAP
@@ -86,14 +91,18 @@
* The identifier of the object represented by the given LDAP entry, or
* null if no attributes declared as containing the identifier of the
* object are present on the entry.
+ *
+ * @throws LdapInvalidAttributeValueException
+ * If an error occurs retrieving the value of the identifier attribute.
*/
- public String getIdentifier(LDAPEntry entry, Collection<String> attributes) {
+ public String getIdentifier(Entry entry, Collection<String> attributes)
+ throws LdapInvalidAttributeValueException {
// Retrieve the first value of the highest priority identifier attribute
for (String identifierAttribute : attributes) {
- LDAPAttribute identifier = entry.getAttribute(identifierAttribute);
+ Attribute identifier = entry.get(identifierAttribute);
if (identifier != null)
- return identifier.getStringValue();
+ return identifier.getString();
}
// No identifier attribute is present on the entry
@@ -125,42 +134,25 @@
* An LDAP query which will search for arbitrary LDAP objects having at
* least one of the given attributes set to the specified value.
*/
- public String generateQuery(String filter,
+ public ExprNode generateQuery(ExprNode filter,
Collection<String> attributes, String attributeValue) {
// Build LDAP query for objects having at least one attribute and with
// the given search filter
- StringBuilder ldapQuery = new StringBuilder();
- ldapQuery.append("(&");
- ldapQuery.append(filter);
+ AndNode searchFilter = new AndNode();
+ searchFilter.addNode(filter);
// Include all attributes within OR clause if there are more than one
- if (attributes.size() > 1)
- ldapQuery.append("(|");
-
+ OrNode attributeFilter = new OrNode();
+
// Add equality comparison for each possible attribute
- for (String attribute : attributes) {
- ldapQuery.append("(");
- ldapQuery.append(escapingService.escapeLDAPSearchFilter(attribute));
+ attributes.forEach(attribute ->
+ attributeFilter.addNode(new EqualityNode(attribute, attributeValue))
+ );
- if (attributeValue != null) {
- ldapQuery.append("=");
- ldapQuery.append(escapingService.escapeLDAPSearchFilter(attributeValue));
- ldapQuery.append(")");
- }
- else
- ldapQuery.append("=*)");
-
- }
-
- // Close OR clause, if any
- if (attributes.size() > 1)
- ldapQuery.append(")");
-
- // Close overall query (AND clause)
- ldapQuery.append(")");
-
- return ldapQuery.toString();
+ searchFilter.addNode(attributeFilter);
+
+ return searchFilter;
}
@@ -178,6 +170,10 @@
*
* @param query
* The LDAP query to execute.
+ *
+ * @param searchHop
+ * The current level of referral depth for this search, used for
+ * limiting the maximum depth to which referrals can go.
*
* @return
* A list of all results accessible to the user currently bound under
@@ -188,43 +184,40 @@
* information required to execute the query cannot be read from
* guacamole.properties.
*/
- public List<LDAPEntry> search(LDAPConnection ldapConnection,
- String baseDN, String query) throws GuacamoleException {
+ public List<Entry> search(LdapNetworkConnection ldapConnection,
+ Dn baseDN, ExprNode query, int searchHop) throws GuacamoleException {
logger.debug("Searching \"{}\" for objects matching \"{}\".", baseDN, query);
- try {
+ LdapConnectionConfig ldapConnectionConfig = ldapConnection.getConfig();
+
+ // Search within subtree of given base DN
+ SearchRequest request = ldapService.getSearchRequest(baseDN,
+ query);
+
+ // Produce list of all entries in the search result, automatically
+ // following referrals if configured to do so
+ List<Entry> entries = new ArrayList<>();
+
+ try (SearchCursor results = ldapConnection.search(request)) {
+ while (results.next()) {
- // Search within subtree of given base DN
- LDAPSearchResults results = ldapConnection.search(baseDN,
- LDAPConnection.SCOPE_SUB, query, null, false,
- confService.getLDAPSearchConstraints());
-
- // Produce list of all entries in the search result, automatically
- // following referrals if configured to do so
- List<LDAPEntry> entries = new ArrayList<>(results.getCount());
- while (results.hasMore()) {
-
- try {
- entries.add(results.next());
+ if (results.isEntry()) {
+ entries.add(results.getEntry());
}
+ else if (results.isReferral() && request.isFollowReferrals()) {
- // Warn if referrals cannot be followed
- catch (LDAPReferralException e) {
- if (confService.getFollowReferrals()) {
- logger.error("Could not follow referral: {}", e.getFailedReferral());
- logger.debug("Error encountered trying to follow referral.", e);
- throw new GuacamoleServerException("Could not follow LDAP referral.", e);
+ Referral referral = results.getReferral();
+ for (String url : referral.getLdapUrls()) {
+ LdapNetworkConnection referralConnection =
+ ldapService.getReferralConnection(
+ new LdapUrl(url),
+ ldapConnectionConfig, searchHop++
+ );
+ entries.addAll(search(referralConnection, baseDN, query,
+ searchHop));
}
- else {
- logger.warn("Given a referral, but referrals are disabled. Error was: {}", e.getMessage());
- logger.debug("Got a referral, but configured to not follow them.", e);
- }
- }
-
- catch (LDAPException e) {
- logger.warn("Failed to process an LDAP search result. Error was: {}", e.resultCodeToString());
- logger.debug("Error processing LDAPEntry search result.", e);
+
}
}
@@ -232,7 +225,7 @@
return entries;
}
- catch (LDAPException | GuacamoleException e) {
+ catch (CursorException | IOException | LdapException e) {
throw new GuacamoleServerException("Unable to query list of "
+ "objects from LDAP directory.", e);
}
@@ -274,11 +267,11 @@
* information required to execute the query cannot be read from
* guacamole.properties.
*/
- public List<LDAPEntry> search(LDAPConnection ldapConnection, String baseDN,
- String filter, Collection<String> attributes, String attributeValue)
+ public List<Entry> search(LdapNetworkConnection ldapConnection, Dn baseDN,
+ ExprNode filter, Collection<String> attributes, String attributeValue)
throws GuacamoleException {
- String query = generateQuery(filter, attributes, attributeValue);
- return search(ldapConnection, baseDN, query);
+ ExprNode query = generateQuery(filter, attributes, attributeValue);
+ return search(ldapConnection, baseDN, query, 0);
}
/**
@@ -302,15 +295,15 @@
* {@link Map} under its corresponding identifier.
*/
public <ObjectType extends Identifiable> Map<String, ObjectType>
- asMap(List<LDAPEntry> entries, Function<LDAPEntry, ObjectType> mapper) {
+ asMap(List<Entry> entries, Function<Entry, ObjectType> mapper) {
// Convert each entry to the corresponding Guacamole API object
Map<String, ObjectType> objects = new HashMap<>(entries.size());
- for (LDAPEntry entry : entries) {
+ for (Entry entry : entries) {
ObjectType object = mapper.apply(entry);
if (object == null) {
- logger.debug("Ignoring object \"{}\".", entry.getDN());
+ logger.debug("Ignoring object \"{}\".", entry.getDn().toString());
continue;
}
@@ -320,7 +313,7 @@
if (objects.putIfAbsent(identifier, object) != null)
logger.warn("Multiple objects ambiguously map to the "
+ "same identifier (\"{}\"). Ignoring \"{}\" as "
- + "a duplicate.", identifier, entry.getDN());
+ + "a duplicate.", identifier, entry.getDn().toString());
}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java
deleted file mode 100644
index a5e359a..0000000
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * 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.ldap;
-
-import com.novell.ldap.LDAPAuthHandler;
-import com.novell.ldap.LDAPAuthProvider;
-import java.io.UnsupportedEncodingException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-/**
- * Class that implements the necessary authentication handling
- * for following referrals in LDAP connections.
- */
-public class ReferralAuthHandler implements LDAPAuthHandler {
-
- /**
- * Logger for this class.
- */
- private final Logger logger = LoggerFactory.getLogger(ReferralAuthHandler.class);
-
- /**
- * The LDAPAuthProvider object that will be set and returned to the referral handler.
- */
- private final LDAPAuthProvider ldapAuth;
-
- /**
- * Creates a ReferralAuthHandler object to handle authentication when
- * following referrals in a LDAP connection, using the provided dn and
- * password.
- *
- * @param dn
- * The distinguished name to use for the referral login.
- *
- * @param password
- * The password to use for the referral login.
- */
- public ReferralAuthHandler(String dn, String password) {
- byte[] passwordBytes;
- try {
-
- // Convert password into corresponding byte array
- if (password != null)
- passwordBytes = password.getBytes("UTF-8");
- else
- passwordBytes = null;
-
- }
- catch (UnsupportedEncodingException e) {
- logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage());
- logger.debug("Support for UTF-8 (as required by Java spec) not found.", e);
- throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
- }
- ldapAuth = new LDAPAuthProvider(dn, passwordBytes);
- }
-
- @Override
- public LDAPAuthProvider getAuthProvider(String host, int port) {
- return ldapAuth;
- }
-
-}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConfigurationService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java
similarity index 87%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConfigurationService.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java
index e8ea0ac..588c60d 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ConfigurationService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/ConfigurationService.java
@@ -17,16 +17,17 @@
* under the License.
*/
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
import com.google.inject.Inject;
-import com.novell.ldap.LDAPSearchConstraints;
import java.util.Collections;
import java.util.List;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.PresenceNode;
+import org.apache.directory.api.ldap.model.message.AliasDerefMode;
+import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.environment.Environment;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* Service for retrieving configuration information regarding the LDAP server.
@@ -34,11 +35,6 @@
public class ConfigurationService {
/**
- * Logger for this class.
- */
- private final Logger logger = LoggerFactory.getLogger(ConfigurationService.class);
-
- /**
* The Guacamole server environment.
*/
@Inject
@@ -113,7 +109,7 @@
* If guacamole.properties cannot be parsed, or if the user base DN
* property is not specified.
*/
- public String getUserBaseDN() throws GuacamoleException {
+ public Dn getUserBaseDN() throws GuacamoleException {
return environment.getRequiredProperty(
LDAPGuacamoleProperties.LDAP_USER_BASE_DN
);
@@ -132,7 +128,7 @@
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
- public String getConfigurationBaseDN() throws GuacamoleException {
+ public Dn getConfigurationBaseDN() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_CONFIG_BASE_DN
);
@@ -168,7 +164,7 @@
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
- public String getGroupBaseDN() throws GuacamoleException {
+ public Dn getGroupBaseDN() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_GROUP_BASE_DN
);
@@ -187,7 +183,7 @@
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
- public String getSearchBindDN() throws GuacamoleException {
+ public Dn getSearchBindDN() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_SEARCH_BIND_DN
);
@@ -242,7 +238,7 @@
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
- private int getMaxResults() throws GuacamoleException {
+ public int getMaxResults() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_MAX_SEARCH_RESULTS,
1000
@@ -262,10 +258,10 @@
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
- private DereferenceAliasesMode getDereferenceAliases() throws GuacamoleException {
+ public AliasDerefMode getDereferenceAliases() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_DEREFERENCE_ALIASES,
- DereferenceAliasesMode.NEVER
+ AliasDerefMode.NEVER_DEREF_ALIASES
);
}
@@ -288,28 +284,8 @@
}
/**
- * Returns a set of LDAPSearchConstraints to apply globally
- * to all LDAP searches.
- *
- * @return
- * A LDAPSearchConstraints object containing constraints
- * to be applied to all LDAP search operations.
- *
- * @throws GuacamoleException
- * If guacamole.properties cannot be parsed.
- */
- public LDAPSearchConstraints getLDAPSearchConstraints() throws GuacamoleException {
-
- LDAPSearchConstraints constraints = new LDAPSearchConstraints();
-
- constraints.setMaxResults(getMaxResults());
- constraints.setDereference(getDereferenceAliases().DEREF_VALUE);
-
- return constraints;
- }
-
- /**
- * Returns the maximum number of referral hops to follow.
+ * Returns the maximum number of referral hops to follow. By default
+ * a maximum of 5 hops is allowed.
*
* @return
* The maximum number of referral hops to follow
@@ -328,20 +304,20 @@
/**
* Returns the search filter that should be used when querying the
* LDAP server for Guacamole users. If no filter is specified,
- * a default of "(objectClass=*)" is returned.
+ * a default of "(objectClass=user)" is returned.
*
* @return
* The search filter that should be used when querying the
* LDAP server for users that are valid in Guacamole, or
- * "(objectClass=*)" if not specified.
+ * "(objectClass=user)" if not specified.
*
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
- public String getUserSearchFilter() throws GuacamoleException {
+ public ExprNode getUserSearchFilter() throws GuacamoleException {
return environment.getProperty(
LDAPGuacamoleProperties.LDAP_USER_SEARCH_FILTER,
- "(objectClass=*)"
+ new PresenceNode("objectClass")
);
}
@@ -363,7 +339,8 @@
}
/**
- * Returns names for custom LDAP user attributes.
+ * Returns names for custom LDAP user attributes. By default no
+ * attributes will be returned.
*
* @return
* Custom LDAP user attributes as configured in guacamole.properties.
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesProperty.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DereferenceAliasesProperty.java
similarity index 75%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesProperty.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DereferenceAliasesProperty.java
index 60b89c4..b33aa19 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/DereferenceAliasesProperty.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/DereferenceAliasesProperty.java
@@ -17,21 +17,22 @@
* under the License.
*/
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
+import org.apache.directory.api.ldap.model.message.AliasDerefMode;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.properties.GuacamoleProperty;
/**
- * A GuacamoleProperty with a value of DereferenceAliases. The possible strings
- * "never", "searching", "finding", and "always" are mapped to their values as a
- * DereferenceAliases enum. Anything else results in a parse error.
+ * A GuacamoleProperty with a value of AliasDerefMode. The possible strings
+ * "never", "searching", "finding", and "always" are mapped to their values as
+ * an AliasDerefMode object. Anything else results in a parse error.
*/
-public abstract class DereferenceAliasesProperty implements GuacamoleProperty<DereferenceAliasesMode> {
+public abstract class DereferenceAliasesProperty implements GuacamoleProperty<AliasDerefMode> {
@Override
- public DereferenceAliasesMode parseValue(String value) throws GuacamoleException {
+ public AliasDerefMode parseValue(String value) throws GuacamoleException {
// No value provided, so return null.
if (value == null)
@@ -39,19 +40,19 @@
// Never dereference aliases
if (value.equals("never"))
- return DereferenceAliasesMode.NEVER;
+ return AliasDerefMode.NEVER_DEREF_ALIASES;
// Dereference aliases during search operations, but not at base
if (value.equals("searching"))
- return DereferenceAliasesMode.SEARCHING;
+ return AliasDerefMode.DEREF_IN_SEARCHING;
// Dereference aliases to locate base, but not during searches
if (value.equals("finding"))
- return DereferenceAliasesMode.FINDING;
+ return AliasDerefMode.DEREF_FINDING_BASE_OBJ;
// Always dereference aliases
if (value.equals("always"))
- return DereferenceAliasesMode.ALWAYS;
+ return AliasDerefMode.DEREF_ALWAYS;
// Anything else is invalid and results in an error
throw new GuacamoleServerException("Dereference aliases must be one of \"never\", \"searching\", \"finding\", or \"always\".");
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethod.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethod.java
similarity index 97%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethod.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethod.java
index 6ae5b01..95c93af 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethod.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethod.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
/**
* All possible encryption methods which may be used when connecting to an LDAP
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethodProperty.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethodProperty.java
similarity index 97%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethodProperty.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethodProperty.java
index 5753756..d76cc4d 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/EncryptionMethodProperty.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/EncryptionMethodProperty.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPGuacamoleProperties.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java
similarity index 82%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPGuacamoleProperties.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java
index 7529956..e5f44f0 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPGuacamoleProperties.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LDAPGuacamoleProperties.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
@@ -39,7 +39,8 @@
/**
* The base DN to search for Guacamole configurations.
*/
- public static final StringGuacamoleProperty LDAP_CONFIG_BASE_DN = new StringGuacamoleProperty() {
+ public static final LdapDnGuacamoleProperty LDAP_CONFIG_BASE_DN =
+ new LdapDnGuacamoleProperty() {
@Override
public String getName() { return "ldap-config-base-dn"; }
@@ -52,7 +53,8 @@
* credentials for querying other LDAP users, all users must be direct
* children of this base DN, varying only by LDAP_USERNAME_ATTRIBUTE.
*/
- public static final StringGuacamoleProperty LDAP_USER_BASE_DN = new StringGuacamoleProperty() {
+ public static final LdapDnGuacamoleProperty LDAP_USER_BASE_DN =
+ new LdapDnGuacamoleProperty() {
@Override
public String getName() { return "ldap-user-base-dn"; }
@@ -64,7 +66,8 @@
* will be used for RBAC must be contained somewhere within the subtree of
* this DN.
*/
- public static final StringGuacamoleProperty LDAP_GROUP_BASE_DN = new StringGuacamoleProperty() {
+ public static final LdapDnGuacamoleProperty LDAP_GROUP_BASE_DN =
+ new LdapDnGuacamoleProperty() {
@Override
public String getName() { return "ldap-group-base-dn"; }
@@ -79,7 +82,8 @@
* one attribute, and the concatenation of that attribute and the value of
* LDAP_USER_BASE_DN must equal the user's full DN.
*/
- public static final StringListProperty LDAP_USERNAME_ATTRIBUTE = new StringListProperty() {
+ public static final StringListProperty LDAP_USERNAME_ATTRIBUTE =
+ new StringListProperty() {
@Override
public String getName() { return "ldap-username-attribute"; }
@@ -91,7 +95,8 @@
* attributes must be present within each Guacamole user group's record in
* the LDAP directory for that group to be visible.
*/
- public static final StringListProperty LDAP_GROUP_NAME_ATTRIBUTE = new StringListProperty() {
+ public static final StringListProperty LDAP_GROUP_NAME_ATTRIBUTE =
+ new StringListProperty() {
@Override
public String getName() { return "ldap-group-name-attribute"; }
@@ -101,7 +106,8 @@
/**
* The port on the LDAP server to connect to when authenticating users.
*/
- public static final IntegerGuacamoleProperty LDAP_PORT = new IntegerGuacamoleProperty() {
+ public static final IntegerGuacamoleProperty LDAP_PORT =
+ new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-port"; }
@@ -111,7 +117,8 @@
/**
* The hostname of the LDAP server to connect to when authenticating users.
*/
- public static final StringGuacamoleProperty LDAP_HOSTNAME = new StringGuacamoleProperty() {
+ public static final StringGuacamoleProperty LDAP_HOSTNAME =
+ new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-hostname"; }
@@ -124,7 +131,8 @@
* specified, the DNs of users attempting to log in will be derived from
* the LDAP_BASE_DN and LDAP_USERNAME_ATTRIBUTE directly.
*/
- public static final StringGuacamoleProperty LDAP_SEARCH_BIND_DN = new StringGuacamoleProperty() {
+ public static final LdapDnGuacamoleProperty LDAP_SEARCH_BIND_DN =
+ new LdapDnGuacamoleProperty() {
@Override
public String getName() { return "ldap-search-bind-dn"; }
@@ -137,7 +145,8 @@
* property has no effect. If this property is not specified, no password
* will be provided when attempting to bind as LDAP_SEARCH_BIND_DN.
*/
- public static final StringGuacamoleProperty LDAP_SEARCH_BIND_PASSWORD = new StringGuacamoleProperty() {
+ public static final StringGuacamoleProperty LDAP_SEARCH_BIND_PASSWORD =
+ new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-search-bind-password"; }
@@ -149,7 +158,8 @@
* The chosen method will also dictate the default port if not already
* explicitly specified via LDAP_PORT.
*/
- public static final EncryptionMethodProperty LDAP_ENCRYPTION_METHOD = new EncryptionMethodProperty() {
+ public static final EncryptionMethodProperty LDAP_ENCRYPTION_METHOD =
+ new EncryptionMethodProperty() {
@Override
public String getName() { return "ldap-encryption-method"; }
@@ -159,7 +169,8 @@
/**
* The maximum number of results a LDAP query can return.
*/
- public static final IntegerGuacamoleProperty LDAP_MAX_SEARCH_RESULTS = new IntegerGuacamoleProperty() {
+ public static final IntegerGuacamoleProperty LDAP_MAX_SEARCH_RESULTS =
+ new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-max-search-results"; }
@@ -170,7 +181,8 @@
* Property that controls whether or not the LDAP connection follows
* (dereferences) aliases as it searches the tree.
*/
- public static final DereferenceAliasesProperty LDAP_DEREFERENCE_ALIASES = new DereferenceAliasesProperty() {
+ public static final DereferenceAliasesProperty LDAP_DEREFERENCE_ALIASES =
+ new DereferenceAliasesProperty() {
@Override
public String getName() { return "ldap-dereference-aliases"; }
@@ -180,7 +192,8 @@
/**
* A search filter to apply to user LDAP queries.
*/
- public static final StringGuacamoleProperty LDAP_USER_SEARCH_FILTER = new StringGuacamoleProperty() {
+ public static final LdapFilterGuacamoleProperty LDAP_USER_SEARCH_FILTER =
+ new LdapFilterGuacamoleProperty() {
@Override
public String getName() { return "ldap-user-search-filter"; }
@@ -190,7 +203,8 @@
/**
* Whether or not we should follow referrals.
*/
- public static final BooleanGuacamoleProperty LDAP_FOLLOW_REFERRALS = new BooleanGuacamoleProperty() {
+ public static final BooleanGuacamoleProperty LDAP_FOLLOW_REFERRALS =
+ new BooleanGuacamoleProperty() {
@Override
public String getName() { return "ldap-follow-referrals"; }
@@ -200,7 +214,8 @@
/**
* Maximum number of referral hops to follow.
*/
- public static final IntegerGuacamoleProperty LDAP_MAX_REFERRAL_HOPS = new IntegerGuacamoleProperty() {
+ public static final IntegerGuacamoleProperty LDAP_MAX_REFERRAL_HOPS =
+ new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-max-referral-hops"; }
@@ -210,7 +225,8 @@
/**
* Number of seconds to wait for LDAP operations to complete.
*/
- public static final IntegerGuacamoleProperty LDAP_OPERATION_TIMEOUT = new IntegerGuacamoleProperty() {
+ public static final IntegerGuacamoleProperty LDAP_OPERATION_TIMEOUT =
+ new IntegerGuacamoleProperty() {
@Override
public String getName() { return "ldap-operation-timeout"; }
@@ -221,7 +237,8 @@
* Custom attribute or attributes to query from Guacamole user's record in
* the LDAP directory.
*/
- public static final StringListProperty LDAP_USER_ATTRIBUTES = new StringListProperty() {
+ public static final StringListProperty LDAP_USER_ATTRIBUTES =
+ new StringListProperty() {
@Override
public String getName() { return "ldap-user-attributes"; }
@@ -231,7 +248,8 @@
/**
* LDAP attribute used to enumerate members of a group in the LDAP directory.
*/
- public static final StringGuacamoleProperty LDAP_MEMBER_ATTRIBUTE = new StringGuacamoleProperty() {
+ public static final StringGuacamoleProperty LDAP_MEMBER_ATTRIBUTE =
+ new StringGuacamoleProperty() {
@Override
public String getName() { return "ldap-member-attribute"; }
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapDnGuacamoleProperty.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapDnGuacamoleProperty.java
new file mode 100644
index 0000000..c782c97
--- /dev/null
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapDnGuacamoleProperty.java
@@ -0,0 +1,50 @@
+/*
+ * 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.ldap.conf;
+
+import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.properties.GuacamoleProperty;
+
+/**
+ * A GuacamoleProperty that converts a string to a Dn that can be used
+ * in LDAP connections. An exception is thrown if the provided DN is invalid
+ * and cannot be parsed.
+ */
+public abstract class LdapDnGuacamoleProperty implements GuacamoleProperty<Dn> {
+
+ @Override
+ public Dn parseValue(String value) throws GuacamoleException {
+
+ if (value == null)
+ return null;
+
+ try {
+ return new Dn(value);
+ }
+ catch (LdapInvalidDnException e) {
+ throw new GuacamoleServerException("The DN \"" + value + "\" is invalid.", e);
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapFilterGuacamoleProperty.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapFilterGuacamoleProperty.java
new file mode 100644
index 0000000..01b41c9
--- /dev/null
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/LdapFilterGuacamoleProperty.java
@@ -0,0 +1,53 @@
+/*
+ * 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.ldap.conf;
+
+import java.text.ParseException;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.FilterParser;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.properties.GuacamoleProperty;
+
+/**
+ * A GuacamoleProperty with a value of an ExprNode query filter. The string
+ * provided is passed through the FilterParser returning the ExprNode object,
+ * or an exception is thrown if the filter is invalid and cannot be correctly
+ * parsed.
+ */
+public abstract class LdapFilterGuacamoleProperty implements GuacamoleProperty<ExprNode> {
+
+ @Override
+ public ExprNode parseValue(String value) throws GuacamoleException {
+
+ // No value provided, so return null.
+ if (value == null)
+ return null;
+
+ try {
+ return FilterParser.parse(value);
+ }
+ catch (ParseException e) {
+ throw new GuacamoleServerException("\"" + value + "\" is not a valid LDAP filter.", e);
+ }
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/StringListProperty.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/StringListProperty.java
similarity index 97%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/StringListProperty.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/StringListProperty.java
index 908d922..f7057e9 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/StringListProperty.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/conf/StringListProperty.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.auth.ldap.conf;
import java.util.Arrays;
import java.util.List;
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java
index 2f2b674..6b2d840 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/connection/ConnectionService.java
@@ -20,17 +20,22 @@
package org.apache.guacamole.auth.ldap.connection;
import com.google.inject.Inject;
-import com.novell.ldap.LDAPAttribute;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
-import com.novell.ldap.LDAPException;
import java.util.Collections;
-import java.util.Enumeration;
import java.util.List;
import java.util.Map;
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
+import org.apache.directory.api.ldap.model.filter.AndNode;
+import org.apache.directory.api.ldap.model.filter.EqualityNode;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.OrNode;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.ldap.client.api.LdapConnectionConfig;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider;
-import org.apache.guacamole.auth.ldap.ConfigurationService;
-import org.apache.guacamole.auth.ldap.EscapingService;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.auth.ldap.ObjectQueryService;
@@ -53,13 +58,7 @@
/**
* Logger for this class.
*/
- private final Logger logger = LoggerFactory.getLogger(ConnectionService.class);
-
- /**
- * Service for escaping parts of LDAP queries.
- */
- @Inject
- private EscapingService escapingService;
+ private static final Logger logger = LoggerFactory.getLogger(ConnectionService.class);
/**
* Service for retrieving LDAP server configuration information.
@@ -100,17 +99,18 @@
* If an error occurs preventing retrieval of connections.
*/
public Map<String, Connection> getConnections(AuthenticatedUser user,
- LDAPConnection ldapConnection) throws GuacamoleException {
+ LdapNetworkConnection ldapConnection) throws GuacamoleException {
// Do not return any connections if base DN is not specified
- String configurationBaseDN = confService.getConfigurationBaseDN();
+ Dn configurationBaseDN = confService.getConfigurationBaseDN();
if (configurationBaseDN == null)
return Collections.<String, Connection>emptyMap();
try {
// Pull the current user DN from the LDAP connection
- String userDN = ldapConnection.getAuthenticationDN();
+ LdapConnectionConfig ldapConnectionConfig = ldapConnection.getConfig();
+ Dn userDN = new Dn(ldapConnectionConfig.getName());
// getConnections() will only be called after a connection has been
// authenticated (via non-anonymous bind), thus userDN cannot
@@ -119,46 +119,77 @@
// Get the search filter for finding connections accessible by the
// current user
- String connectionSearchFilter = getConnectionSearchFilter(userDN, ldapConnection);
+ ExprNode connectionSearchFilter = getConnectionSearchFilter(userDN, ldapConnection);
// Find all Guacamole connections for the given user by
// looking for direct membership in the guacConfigGroup
// and possibly any groups the user is a member of that are
// referred to in the seeAlso attribute of the guacConfigGroup.
- List<LDAPEntry> results = queryService.search(ldapConnection, configurationBaseDN, connectionSearchFilter);
+ List<Entry> results = queryService.search(ldapConnection,
+ configurationBaseDN, connectionSearchFilter, 0);
// Return a map of all readable connections
return queryService.asMap(results, (entry) -> {
// Get common name (CN)
- LDAPAttribute cn = entry.getAttribute("cn");
+ Attribute cn = entry.get("cn");
+
if (cn == null) {
logger.warn("guacConfigGroup is missing a cn.");
return null;
}
+
+ String cnName;
+
+ try {
+ cnName = cn.getString();
+ }
+ catch (LdapInvalidAttributeValueException e) {
+ logger.error("Invalid value for CN attribute: {}",
+ e.getMessage());
+ logger.debug("LDAP exception while getting CN attribute.", e);
+ return null;
+ }
// Get associated protocol
- LDAPAttribute protocol = entry.getAttribute("guacConfigProtocol");
+ Attribute protocol = entry.get("guacConfigProtocol");
if (protocol == null) {
logger.warn("guacConfigGroup \"{}\" is missing the "
+ "required \"guacConfigProtocol\" attribute.",
- cn.getStringValue());
+ cnName);
return null;
}
// Set protocol
GuacamoleConfiguration config = new GuacamoleConfiguration();
- config.setProtocol(protocol.getStringValue());
+ try {
+ config.setProtocol(protocol.getString());
+ }
+ catch (LdapInvalidAttributeValueException e) {
+ logger.error("Invalid value of the protocol entry: {}",
+ e.getMessage());
+ logger.debug("LDAP exception when getting protocol value.", e);
+ return null;
+ }
// Get parameters, if any
- LDAPAttribute parameterAttribute = entry.getAttribute("guacConfigParameter");
+ Attribute parameterAttribute = entry.get("guacConfigParameter");
if (parameterAttribute != null) {
// For each parameter
- Enumeration<?> parameters = parameterAttribute.getStringValues();
- while (parameters.hasMoreElements()) {
-
- String parameter = (String) parameters.nextElement();
+ while (parameterAttribute.size() > 0) {
+ String parameter;
+ try {
+ parameter = parameterAttribute.getString();
+ }
+ catch (LdapInvalidAttributeValueException e) {
+ logger.warn("Parameter value not valid for {}: {}",
+ cnName, e.getMessage());
+ logger.debug("LDAP exception when getting parameter value.",
+ e);
+ return null;
+ }
+ parameterAttribute.remove(parameter);
// Parse parameter
int equals = parameter.indexOf('=');
@@ -177,8 +208,7 @@
}
// Store connection using cn for both identifier and name
- String name = cn.getStringValue();
- Connection connection = new SimpleConnection(name, name, config, true);
+ Connection connection = new SimpleConnection(cnName, cnName, config, true);
connection.setParentIdentifier(LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP);
// Inject LDAP-specific tokens only if LDAP handled user
@@ -192,7 +222,7 @@
});
}
- catch (LDAPException e) {
+ catch (LdapException e) {
throw new GuacamoleServerException("Error while querying for connections.", e);
}
@@ -213,40 +243,39 @@
* An LDAP search filter which queries all guacConfigGroup objects
* accessible by the user having the given DN.
*
- * @throws LDAPException
+ * @throws LdapException
* If an error occurs preventing retrieval of user groups.
*
* @throws GuacamoleException
* If an error occurs retrieving the group base DN.
*/
- private String getConnectionSearchFilter(String userDN,
- LDAPConnection ldapConnection)
- throws LDAPException, GuacamoleException {
+ private ExprNode getConnectionSearchFilter(Dn userDN,
+ LdapNetworkConnection ldapConnection)
+ throws LdapException, GuacamoleException {
- // Create a search filter for the connection search
- StringBuilder connectionSearchFilter = new StringBuilder();
+ AndNode searchFilter = new AndNode();
// Add the prefix to the search filter, prefix filter searches for guacConfigGroups with the userDN as the member attribute value
- connectionSearchFilter.append("(&(objectClass=guacConfigGroup)");
- connectionSearchFilter.append("(|(");
- connectionSearchFilter.append(escapingService.escapeLDAPSearchFilter(
- confService.getMemberAttribute()));
- connectionSearchFilter.append("=");
- connectionSearchFilter.append(escapingService.escapeLDAPSearchFilter(userDN));
- connectionSearchFilter.append(")");
+ searchFilter.addNode(new EqualityNode("objectClass","guacConfigGroup"));
+
+ // Apply group filters
+ OrNode groupFilter = new OrNode();
+ groupFilter.addNode(new EqualityNode(confService.getMemberAttribute(),
+ userDN.toString()));
// Additionally filter by group membership if the current user is a
// member of any user groups
- List<LDAPEntry> userGroups = userGroupService.getParentUserGroupEntries(ldapConnection, userDN);
+ List<Entry> userGroups = userGroupService.getParentUserGroupEntries(ldapConnection, userDN);
if (!userGroups.isEmpty()) {
- for (LDAPEntry entry : userGroups)
- connectionSearchFilter.append("(seeAlso=").append(escapingService.escapeLDAPSearchFilter(entry.getDN())).append(")");
+ userGroups.forEach(entry ->
+ groupFilter.addNode(new EqualityNode("seeAlso",entry.getDn().toString()))
+ );
}
// Complete the search filter.
- connectionSearchFilter.append("))");
+ searchFilter.addNode(groupFilter);
- return connectionSearchFilter.toString();
+ return searchFilter;
}
}
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java
index 3315beb..0628006 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/group/UserGroupService.java
@@ -20,15 +20,21 @@
package org.apache.guacamole.auth.ldap.group;
import com.google.inject.Inject;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import org.apache.guacamole.auth.ldap.ConfigurationService;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
+import org.apache.directory.api.ldap.model.filter.EqualityNode;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.NotNode;
+import org.apache.directory.api.ldap.model.filter.PresenceNode;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.ObjectQueryService;
import org.apache.guacamole.net.auth.UserGroup;
@@ -45,7 +51,7 @@
/**
* Logger for this class.
*/
- private final Logger logger = LoggerFactory.getLogger(UserGroupService.class);
+ private static final Logger logger = LoggerFactory.getLogger(UserGroupService.class);
/**
* Service for retrieving LDAP server configuration information.
@@ -72,17 +78,17 @@
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
- private String getGroupSearchFilter() throws GuacamoleException {
+ private ExprNode getGroupSearchFilter() throws GuacamoleException {
// Explicitly exclude guacConfigGroup object class only if it should
// be assumed to be defined (query may fail due to no such object
// class existing otherwise)
if (confService.getConfigurationBaseDN() != null)
- return "(!(objectClass=guacConfigGroup))";
+ return new NotNode(new EqualityNode("objectClass","guacConfigGroup"));
// Read any object as a group if LDAP is not being used for connection
// storage (guacConfigGroup)
- return "(objectClass=*)";
+ return new PresenceNode("objectClass");
}
@@ -102,17 +108,17 @@
* @throws GuacamoleException
* If an error occurs preventing retrieval of user groups.
*/
- public Map<String, UserGroup> getUserGroups(LDAPConnection ldapConnection)
+ public Map<String, UserGroup> getUserGroups(LdapNetworkConnection ldapConnection)
throws GuacamoleException {
// Do not return any user groups if base DN is not specified
- String groupBaseDN = confService.getGroupBaseDN();
+ Dn groupBaseDN = confService.getGroupBaseDN();
if (groupBaseDN == null)
return Collections.emptyMap();
// Retrieve all visible user groups which are not guacConfigGroups
Collection<String> attributes = confService.getGroupNameAttributes();
- List<LDAPEntry> results = queryService.search(
+ List<Entry> results = queryService.search(
ldapConnection,
groupBaseDN,
getGroupSearchFilter(),
@@ -125,13 +131,18 @@
return queryService.asMap(results, entry -> {
// Translate entry into UserGroup object having proper identifier
- String name = queryService.getIdentifier(entry, attributes);
- if (name != null)
- return new SimpleUserGroup(name);
+ try {
+ String name = queryService.getIdentifier(entry, attributes);
+ if (name != null)
+ return new SimpleUserGroup(name);
+ }
+ catch (LdapInvalidAttributeValueException e) {
+ return null;
+ }
// Ignore user groups which lack a name attribute
logger.debug("User group \"{}\" is missing a name attribute "
- + "and will be ignored.", entry.getDN());
+ + "and will be ignored.", entry.getDn().toString());
return null;
});
@@ -157,11 +168,11 @@
* @throws GuacamoleException
* If an error occurs preventing retrieval of user groups.
*/
- public List<LDAPEntry> getParentUserGroupEntries(LDAPConnection ldapConnection,
- String userDN) throws GuacamoleException {
+ public List<Entry> getParentUserGroupEntries(LdapNetworkConnection ldapConnection,
+ Dn userDN) throws GuacamoleException {
// Do not return any user groups if base DN is not specified
- String groupBaseDN = confService.getGroupBaseDN();
+ Dn groupBaseDN = confService.getGroupBaseDN();
if (groupBaseDN == null)
return Collections.emptyList();
@@ -172,7 +183,7 @@
groupBaseDN,
getGroupSearchFilter(),
Collections.singleton(confService.getMemberAttribute()),
- userDN
+ userDN.toString()
);
}
@@ -196,24 +207,31 @@
* @throws GuacamoleException
* If an error occurs preventing retrieval of user groups.
*/
- public Set<String> getParentUserGroupIdentifiers(LDAPConnection ldapConnection,
- String userDN) throws GuacamoleException {
+ public Set<String> getParentUserGroupIdentifiers(LdapNetworkConnection ldapConnection,
+ Dn userDN) throws GuacamoleException {
Collection<String> attributes = confService.getGroupNameAttributes();
- List<LDAPEntry> userGroups = getParentUserGroupEntries(ldapConnection, userDN);
+ List<Entry> userGroups = getParentUserGroupEntries(ldapConnection, userDN);
Set<String> identifiers = new HashSet<>(userGroups.size());
userGroups.forEach(entry -> {
// Determine unique identifier for user group
- String name = queryService.getIdentifier(entry, attributes);
- if (name != null)
- identifiers.add(name);
+ try {
+ String name = queryService.getIdentifier(entry, attributes);
+ if (name != null)
+ identifiers.add(name);
- // Ignore user groups which lack a name attribute
- else
- logger.debug("User group \"{}\" is missing a name attribute "
- + "and will be ignored.", entry.getDN());
+ // Ignore user groups which lack a name attribute
+ else
+ logger.debug("User group \"{}\" is missing a name attribute "
+ + "and will be ignored.", entry.getDn().toString());
+ }
+ catch (LdapInvalidAttributeValueException e) {
+ logger.error("User group missing identifier: {}",
+ e.getMessage());
+ logger.debug("LDAP exception while getting group identifier.", e);
+ }
});
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java
index cafc461..4429643 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java
@@ -23,6 +23,7 @@
import java.util.Collections;
import java.util.Map;
import java.util.Set;
+import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.auth.Credentials;
@@ -56,6 +57,11 @@
* available to this user.
*/
private Set<String> effectiveGroups;
+
+ /**
+ * The LDAP DN used to bind this user.
+ */
+ private Dn bindDn;
/**
* Initializes this AuthenticatedUser with the given credentials,
@@ -71,14 +77,19 @@
* @param effectiveGroups
* The unique identifiers of all user groups which affect the
* permissions available to this user.
+ *
+ * @param bindDn
+ * The LDAP DN used to bind this user.
*/
- public void init(Credentials credentials, Map<String, String> tokens, Set<String> effectiveGroups) {
+ public void init(Credentials credentials, Map<String, String> tokens,
+ Set<String> effectiveGroups, Dn bindDn) {
this.credentials = credentials;
this.tokens = Collections.unmodifiableMap(tokens);
this.effectiveGroups = effectiveGroups;
+ this.bindDn = bindDn;
setIdentifier(credentials.getUsername());
}
-
+
/**
* Returns a Map of all name/value pairs that should be applied as
* parameter tokens when connections are established using this
@@ -92,6 +103,16 @@
public Map<String, String> getTokens() {
return tokens;
}
+
+ /**
+ * Returns the LDAP DN used to bind this user.
+ *
+ * @return
+ * The LDAP DN used to bind this user.
+ */
+ public Dn getBindDn() {
+ return bindDn;
+ }
@Override
public AuthenticationProvider getAuthenticationProvider() {
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java
index 5505f7e..b5c789e 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java
@@ -20,8 +20,8 @@
package org.apache.guacamole.auth.ldap.user;
import com.google.inject.Inject;
-import com.novell.ldap.LDAPConnection;
import java.util.Collections;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.guacamole.auth.ldap.connection.ConnectionService;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.auth.ldap.LDAPAuthenticationProvider;
@@ -39,8 +39,6 @@
import org.apache.guacamole.net.auth.simple.SimpleDirectory;
import org.apache.guacamole.net.auth.simple.SimpleObjectPermissionSet;
import org.apache.guacamole.net.auth.simple.SimpleUser;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
/**
* An LDAP-specific implementation of UserContext which queries all Guacamole
@@ -49,11 +47,6 @@
public class LDAPUserContext extends AbstractUserContext {
/**
- * Logger for this class.
- */
- private final Logger logger = LoggerFactory.getLogger(LDAPUserContext.class);
-
- /**
* Service for retrieving Guacamole connections from the LDAP server.
*/
@Inject
@@ -109,7 +102,7 @@
/**
* Initializes this UserContext using the provided AuthenticatedUser and
- * LDAPConnection.
+ * LdapNetworkConnection.
*
* @param user
* The AuthenticatedUser representing the user that authenticated. This
@@ -124,7 +117,7 @@
* If associated data stored within the LDAP directory cannot be
* queried due to an error.
*/
- public void init(AuthenticatedUser user, LDAPConnection ldapConnection)
+ public void init(AuthenticatedUser user, LdapNetworkConnection ldapConnection)
throws GuacamoleException {
// Query all accessible users
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java
index 3f12ae8..ba29983 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java
@@ -20,16 +20,20 @@
package org.apache.guacamole.auth.ldap.user;
import com.google.inject.Inject;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
-import org.apache.guacamole.auth.ldap.ConfigurationService;
-import org.apache.guacamole.auth.ldap.EscapingService;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.name.Rdn;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.auth.ldap.LDAPGuacamoleProperties;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.auth.ldap.conf.LDAPGuacamoleProperties;
import org.apache.guacamole.auth.ldap.ObjectQueryService;
import org.apache.guacamole.net.auth.User;
import org.apache.guacamole.net.auth.simple.SimpleUser;
@@ -45,13 +49,7 @@
/**
* Logger for this class.
*/
- private final Logger logger = LoggerFactory.getLogger(UserService.class);
-
- /**
- * Service for escaping parts of LDAP queries.
- */
- @Inject
- private EscapingService escapingService;
+ private static final Logger logger = LoggerFactory.getLogger(UserService.class);
/**
* Service for retrieving LDAP server configuration information.
@@ -81,12 +79,12 @@
* @throws GuacamoleException
* If an error occurs preventing retrieval of users.
*/
- public Map<String, User> getUsers(LDAPConnection ldapConnection)
+ public Map<String, User> getUsers(LdapNetworkConnection ldapConnection)
throws GuacamoleException {
// Retrieve all visible user objects
Collection<String> attributes = confService.getUsernameAttributes();
- List<LDAPEntry> results = queryService.search(ldapConnection,
+ List<Entry> results = queryService.search(ldapConnection,
confService.getUserBaseDN(),
confService.getUserSearchFilter(),
attributes,
@@ -96,15 +94,21 @@
return queryService.asMap(results, entry -> {
// Get username from record
- String username = queryService.getIdentifier(entry, attributes);
- if (username == null) {
- logger.warn("User \"{}\" is missing a username attribute "
- + "and will be ignored.", entry.getDN());
+ try {
+ String username = queryService.getIdentifier(entry, attributes);
+ if (username == null) {
+ logger.warn("User \"{}\" is missing a username attribute "
+ + "and will be ignored.", entry.getDn().toString());
+ return null;
+ }
+
+ return new SimpleUser(username);
+ }
+ catch (LdapInvalidAttributeValueException e) {
+
return null;
}
- return new SimpleUser(username);
-
});
}
@@ -130,19 +134,19 @@
* If an error occurs while querying the user DNs, or if the username
* attribute property cannot be parsed within guacamole.properties.
*/
- public List<String> getUserDNs(LDAPConnection ldapConnection,
+ public List<Dn> getUserDNs(LdapNetworkConnection ldapConnection,
String username) throws GuacamoleException {
// Retrieve user objects having a matching username
- List<LDAPEntry> results = queryService.search(ldapConnection,
+ List<Entry> results = queryService.search(ldapConnection,
confService.getUserBaseDN(),
confService.getUserSearchFilter(),
confService.getUsernameAttributes(),
username);
// Build list of all DNs for retrieved users
- List<String> userDNs = new ArrayList<>(results.size());
- results.forEach(entry -> userDNs.add(entry.getDN()));
+ List<Dn> userDNs = new ArrayList<>(results.size());
+ results.forEach(entry -> userDNs.add(entry.getDn()));
return userDNs;
@@ -164,7 +168,7 @@
* If required properties are missing, and thus the user DN cannot be
* determined.
*/
- public String deriveUserDN(String username)
+ public Dn deriveUserDN(String username)
throws GuacamoleException {
// Pull username attributes from properties
@@ -181,10 +185,13 @@
}
// Derive user DN from base DN
- return
- escapingService.escapeDN(usernameAttributes.get(0))
- + "=" + escapingService.escapeDN(username)
- + "," + confService.getUserBaseDN();
+ try {
+ return new Dn(new Rdn(usernameAttributes.get(0), username),
+ confService.getUserBaseDN());
+ }
+ catch (LdapInvalidAttributeValueException | LdapInvalidDnException e) {
+ throw new GuacamoleServerException("Error trying to derive user DN.", e);
+ }
}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java
index 4fd37f1..fee4357 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java
@@ -25,7 +25,7 @@
import java.util.Arrays;
import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.auth.radius.user.AuthenticatedUser;
-import org.apache.guacamole.auth.radius.form.RadiusChallengeResponseField;
+import org.apache.guacamole.auth.radius.form.GuacamoleRadiusChallenge;
import org.apache.guacamole.auth.radius.form.RadiusStateField;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.form.Field;
@@ -42,6 +42,7 @@
import net.jradius.packet.AccessChallenge;
import net.jradius.packet.AccessReject;
import net.jradius.packet.attribute.RadiusAttribute;
+import org.apache.guacamole.form.PasswordField;
/**
* Service providing convenience functions for the RADIUS AuthenticationProvider
@@ -53,6 +54,12 @@
* Logger for this class.
*/
private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
+
+ /**
+ * The name of the password field where the user will enter a response to
+ * the RADIUS challenge.
+ */
+ private static final String CHALLENGE_RESPONSE_PARAM = "radiusChallenge";
/**
* Service for creating and managing connections to RADIUS servers.
@@ -67,18 +74,23 @@
private Provider<AuthenticatedUser> authenticatedUserProvider;
/**
- * Returns the expected credentials from a RADIUS challenge.
+ * Returns an object containing the challenge message and the expected
+ * credentials from a RADIUS challenge, or null if either state or reply
+ * attributes are missing from the challenge.
*
* @param challengePacket
* The AccessChallenge RadiusPacket received from the RADIUS
* server.
*
* @return
- * A CredentialsInfo object that represents fields that need to
- * be presented to the user in order to complete authentication.
- * One of these must be the RADIUS state.
+ * A GuacamoleRadiusChallenge object that contains the challenge message
+ * sent by the RADIUS server and the expected credentials that should
+ * be requested of the user in order to continue authentication. One
+ * of the expected credentials *must* be the RADIUS state. If either
+ * state or the reply are missing from the challenge this method will
+ * return null.
*/
- private CredentialsInfo getRadiusChallenge(RadiusPacket challengePacket) {
+ private GuacamoleRadiusChallenge getRadiusChallenge(RadiusPacket challengePacket) {
// Try to get the state attribute - if it's not there, we have a problem
RadiusAttribute stateAttr = challengePacket.findAttribute(Attr_State.TYPE);
@@ -97,13 +109,16 @@
}
// We have the required attributes - convert to strings and then generate the additional login box/field
- String replyMsg = replyAttr.toString();
+ String replyMsg = replyAttr.getValue().toString();
String radiusState = BaseEncoding.base16().encode(stateAttr.getValue().getBytes());
- Field radiusResponseField = new RadiusChallengeResponseField(replyMsg);
+ Field radiusResponseField = new PasswordField(CHALLENGE_RESPONSE_PARAM);
Field radiusStateField = new RadiusStateField(radiusState);
- // Return the CredentialsInfo object that has the state and the expected response.
- return new CredentialsInfo(Arrays.asList(radiusResponseField,radiusStateField));
+ // Return the GuacamoleRadiusChallenge object that has the state
+ // and the expected response.
+ return new GuacamoleRadiusChallenge(replyMsg,
+ new CredentialsInfo(Arrays.asList(radiusResponseField,
+ radiusStateField)));
}
/**
@@ -134,7 +149,7 @@
// Grab HTTP request object and a response to a challenge.
HttpServletRequest request = credentials.getRequest();
- String challengeResponse = request.getParameter(RadiusChallengeResponseField.PARAMETER_NAME);
+ String challengeResponse = request.getParameter(CHALLENGE_RESPONSE_PARAM);
// RadiusPacket object to store response from server.
RadiusPacket radPack;
@@ -200,12 +215,14 @@
// Received AccessChallenge packet, more credentials required to complete authentication
else if (radPack instanceof AccessChallenge) {
- CredentialsInfo expectedCredentials = getRadiusChallenge(radPack);
+ GuacamoleRadiusChallenge challenge = getRadiusChallenge(radPack);
- if (expectedCredentials == null)
+ if (challenge == null)
throw new GuacamoleInvalidCredentialsException("Authentication error.", CredentialsInfo.USERNAME_PASSWORD);
- throw new GuacamoleInsufficientCredentialsException("LOGIN.INFO_RADIUS_ADDL_REQUIRED", expectedCredentials);
+ throw new GuacamoleInsufficientCredentialsException(
+ challenge.getChallengeText(),
+ challenge.getExpectedCredentials());
}
// Something unanticipated happened, so panic and go back to login.
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java
index 37ecb79..4224f77 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java
@@ -20,10 +20,17 @@
package org.apache.guacamole.auth.radius;
import com.google.inject.AbstractModule;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Security;
import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.radius.conf.ConfigurationService;
+import org.apache.guacamole.auth.radius.conf.RadiusAuthenticationProtocol;
+import org.apache.guacamole.auth.radius.conf.RadiusGuacamoleProperties;
import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.environment.LocalEnvironment;
import org.apache.guacamole.net.auth.AuthenticationProvider;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
/**
* Guice module which configures RADIUS-specific injections.
@@ -57,6 +64,23 @@
// Get local environment
this.environment = new LocalEnvironment();
+
+ // Check for MD4 requirement
+ RadiusAuthenticationProtocol authProtocol = environment.getProperty(RadiusGuacamoleProperties.RADIUS_AUTH_PROTOCOL);
+ RadiusAuthenticationProtocol innerProtocol = environment.getProperty(RadiusGuacamoleProperties.RADIUS_EAP_TTLS_INNER_PROTOCOL);
+ if (authProtocol == RadiusAuthenticationProtocol.MSCHAPv1
+ || authProtocol == RadiusAuthenticationProtocol.MSCHAPv2
+ || innerProtocol == RadiusAuthenticationProtocol.MSCHAPv1
+ || innerProtocol == RadiusAuthenticationProtocol.MSCHAPv2) {
+
+ try {
+ MessageDigest.getInstance("MD4");
+ }
+ catch (NoSuchAlgorithmException e) {
+ Security.addProvider(new BouncyCastleProvider());
+ }
+
+ }
// Store associated auth provider
this.authProvider = authProvider;
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java
index ec82a63..c8a21d6 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java
@@ -27,6 +27,8 @@
import java.security.NoSuchAlgorithmException;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.auth.radius.conf.ConfigurationService;
+import org.apache.guacamole.auth.radius.conf.RadiusAuthenticationProtocol;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.jradius.client.RadiusClient;
@@ -62,8 +64,7 @@
*/
@Inject
private ConfigurationService confService;
-
-
+
/**
* Creates a new instance of RadiusClient, configured with parameters
* from guacamole.properties.
@@ -115,8 +116,8 @@
* not configured when the client is set up for a tunneled
* RADIUS connection.
*/
- private RadiusAuthenticator setupRadiusAuthenticator(RadiusClient radiusClient)
- throws GuacamoleException {
+ private RadiusAuthenticator setupRadiusAuthenticator(
+ RadiusClient radiusClient) throws GuacamoleException {
// If we don't have a radiusClient object, yet, don't go any further.
if (radiusClient == null) {
@@ -125,7 +126,9 @@
return null;
}
- RadiusAuthenticator radAuth = radiusClient.getAuthProtocol(confService.getRadiusAuthProtocol());
+ RadiusAuthenticator radAuth = radiusClient.getAuthProtocol(
+ confService.getRadiusAuthProtocol().toString());
+
if (radAuth == null)
throw new GuacamoleException("Could not get a valid RadiusAuthenticator for specified protocol: " + confService.getRadiusAuthProtocol());
@@ -157,11 +160,13 @@
// If we're using EAP-TTLS, we need to define tunneled protocol
if (radAuth instanceof EAPTTLSAuthenticator) {
- String innerProtocol = confService.getRadiusEAPTTLSInnerProtocol();
+ RadiusAuthenticationProtocol innerProtocol =
+ confService.getRadiusEAPTTLSInnerProtocol();
+
if (innerProtocol == null)
- throw new GuacamoleException("Trying to use EAP-TTLS, but no inner protocol specified.");
+ throw new GuacamoleException("Missing or invalid inner protocol for EAP-TTLS.");
- ((EAPTTLSAuthenticator)radAuth).setInnerProtocol(innerProtocol);
+ ((EAPTTLSAuthenticator)radAuth).setInnerProtocol(innerProtocol.toString());
}
return radAuth;
@@ -236,14 +241,21 @@
radAuth.setupRequest(radiusClient, radAcc);
radAuth.processRequest(radAcc);
- RadiusResponse reply = radiusClient.sendReceive(radAcc, confService.getRadiusMaxRetries());
+ RadiusResponse reply = radiusClient.sendReceive(radAcc,
+ confService.getRadiusMaxRetries());
// We receive a Challenge not asking for user input, so silently process the challenge
- while((reply instanceof AccessChallenge) && (reply.findAttribute(Attr_ReplyMessage.TYPE) == null)) {
+ while((reply instanceof AccessChallenge)
+ && (reply.findAttribute(Attr_ReplyMessage.TYPE) == null)) {
+
radAuth.processChallenge(radAcc, reply);
- reply = radiusClient.sendReceive(radAcc, confService.getRadiusMaxRetries());
+ reply = radiusClient.sendReceive(radAcc,
+ confService.getRadiusMaxRetries());
+
}
+
return reply;
+
}
catch (RadiusException e) {
logger.error("Unable to complete authentication.", e.getMessage());
@@ -282,8 +294,8 @@
* @throws GuacamoleException
* If an error is encountered trying to talk to the RADIUS server.
*/
- public RadiusPacket sendChallengeResponse(String username, String response, byte[] state)
- throws GuacamoleException {
+ public RadiusPacket sendChallengeResponse(String username, String response,
+ byte[] state) throws GuacamoleException {
if (username == null || username.isEmpty()) {
logger.error("Challenge/response to RADIUS requires a username.");
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/ConfigurationService.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/ConfigurationService.java
similarity index 92%
rename from extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/ConfigurationService.java
rename to extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/ConfigurationService.java
index 381ea13..2809f7c 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/ConfigurationService.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/ConfigurationService.java
@@ -17,11 +17,12 @@
* under the License.
*/
-package org.apache.guacamole.auth.radius;
+package org.apache.guacamole.auth.radius.conf;
import com.google.inject.Inject;
import java.io.File;
import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment;
/**
@@ -123,8 +124,9 @@
* @throws GuacamoleException
* If guacamole.properties cannot be parsed.
*/
- public String getRadiusAuthProtocol() throws GuacamoleException {
- return environment.getProperty(
+ public RadiusAuthenticationProtocol getRadiusAuthProtocol()
+ throws GuacamoleException {
+ return environment.getRequiredProperty(
RadiusGuacamoleProperties.RADIUS_AUTH_PROTOCOL
);
}
@@ -309,12 +311,21 @@
* an EAP-TTLS RADIUS connection.
*
* @throws GuacamoleException
- * If guacamole.properties cannot be parsed.
+ * If guacamole.properties cannot be parsed, or if EAP-TTLS is specified
+ * as the inner protocol.
*/
- public String getRadiusEAPTTLSInnerProtocol() throws GuacamoleException {
- return environment.getProperty(
+ public RadiusAuthenticationProtocol getRadiusEAPTTLSInnerProtocol()
+ throws GuacamoleException {
+
+ RadiusAuthenticationProtocol authProtocol = environment.getProperty(
RadiusGuacamoleProperties.RADIUS_EAP_TTLS_INNER_PROTOCOL
);
+
+ if (authProtocol == RadiusAuthenticationProtocol.EAP_TTLS)
+ throw new GuacamoleServerException("Invalid inner protocol specified for EAP-TTLS.");
+
+ return authProtocol;
+
}
}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocol.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocol.java
new file mode 100644
index 0000000..e64a695
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocol.java
@@ -0,0 +1,118 @@
+/*
+ * 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.radius.conf;
+
+/**
+ * This enum represents supported RADIUS authentication protocols for
+ * the guacamole-auth-radius extension.
+ */
+public enum RadiusAuthenticationProtocol {
+
+ /**
+ * Password Authentication Protocol (PAP)
+ */
+ PAP("pap"),
+
+ /**
+ * Challenge-Handshake Authentication Protocol (CHAP)
+ */
+ CHAP("chap"),
+
+ /**
+ * Microsoft implementation of CHAP, Version 1 (MS-CHAPv1)
+ */
+ MSCHAPv1("mschapv1"),
+
+ /**
+ * Microsoft implementation of CHAP, Version 2 (MS-CHAPv2)
+ */
+ MSCHAPv2("mschapv2"),
+
+ /**
+ * Extensible Authentication Protocol (EAP) with MD5 Hashing (EAP-MD5)
+ */
+ EAP_MD5("eap-md5"),
+
+ /**
+ * Extensible Authentication Protocol (EAP) with TLS encryption (EAP-TLS).
+ */
+ EAP_TLS("eap-tls"),
+
+ /**
+ * Extensible Authentication Protocol (EAP) with Tunneled TLS (EAP-TTLS).
+ */
+ EAP_TTLS("eap-ttls");
+
+ /**
+ * This variable stores the string value of the protocol, and is also
+ * used within the extension to pass to JRadius for configuring the
+ * library to talk to the RADIUS server.
+ */
+ private final String strValue;
+
+ /**
+ * Create a new RadiusAuthenticationProtocol object having the
+ * given string value.
+ *
+ * @param strValue
+ * The value of the protocol to store as a string, which will be used
+ * in specifying the protocol within the guacamole.properties file, and
+ * will also be used by the JRadius library for its configuration.
+ */
+ RadiusAuthenticationProtocol(String strValue) {
+ this.strValue = strValue;
+ }
+
+ /**
+ * {@inheritDoc}
+ * <p>
+ * This function returns the stored string values of the selected RADIUS
+ * protocol, which is used both in Guacamole configuration and also to pass
+ * on to the JRadius library for its configuration.
+ *
+ * @return
+ * The string value stored for the selected RADIUS protocol.
+ */
+ @Override
+ public String toString() {
+ return strValue;
+ }
+
+ /**
+ * For a given String value, return the enum value that matches that string,
+ * or null if no matchi is found.
+ *
+ * @param value
+ * The string value to search for in the list of enums.
+ *
+ * @return
+ * The RadiusAuthenticationProtocol value that is identified by the
+ * provided String value.
+ */
+ public static RadiusAuthenticationProtocol getEnum(String value) {
+
+ for (RadiusAuthenticationProtocol v : values())
+ if(v.toString().equals(value))
+ return v;
+
+ return null;
+ }
+
+}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocolProperty.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocolProperty.java
new file mode 100644
index 0000000..c92c0a3
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocolProperty.java
@@ -0,0 +1,54 @@
+/*
+ * 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.radius.conf;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.properties.GuacamoleProperty;
+
+/**
+ * A GuacamoleProperty whose value is a RadiusAuthenticationProtocol.
+ */
+public abstract class RadiusAuthenticationProtocolProperty
+ implements GuacamoleProperty<RadiusAuthenticationProtocol> {
+
+ @Override
+ public RadiusAuthenticationProtocol parseValue(String value)
+ throws GuacamoleException {
+
+ // Nothing provided, nothing returned
+ if (value == null)
+ return null;
+
+ // Attempt to parse the string value
+ RadiusAuthenticationProtocol authProtocol =
+ RadiusAuthenticationProtocol.getEnum(value);
+
+ // Throw an exception if nothing matched.
+ if (authProtocol == null)
+ throw new GuacamoleServerException(
+ "Invalid or unsupported RADIUS authentication protocol.");
+
+ // Return the answer
+ return authProtocol;
+
+ }
+
+}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusGuacamoleProperties.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusGuacamoleProperties.java
similarity index 93%
rename from extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusGuacamoleProperties.java
rename to extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusGuacamoleProperties.java
index aaa445e..af6839b 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusGuacamoleProperties.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusGuacamoleProperties.java
@@ -17,7 +17,7 @@
* under the License.
*/
-package org.apache.guacamole.auth.radius;
+package org.apache.guacamole.auth.radius.conf;
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
import org.apache.guacamole.properties.FileGuacamoleProperty;
@@ -81,7 +81,8 @@
/**
* The authentication protocol of the RADIUS server to connect to when authenticating users.
*/
- public static final StringGuacamoleProperty RADIUS_AUTH_PROTOCOL = new StringGuacamoleProperty() {
+ public static final RadiusAuthenticationProtocolProperty RADIUS_AUTH_PROTOCOL =
+ new RadiusAuthenticationProtocolProperty() {
@Override
public String getName() { return "radius-auth-protocol"; }
@@ -181,7 +182,8 @@
/**
* The tunneled protocol to use inside a RADIUS EAP-TTLS connection.
*/
- public static final StringGuacamoleProperty RADIUS_EAP_TTLS_INNER_PROTOCOL = new StringGuacamoleProperty() {
+ public static final RadiusAuthenticationProtocolProperty RADIUS_EAP_TTLS_INNER_PROTOCOL =
+ new RadiusAuthenticationProtocolProperty() {
@Override
public String getName() { return "radius-eap-ttls-inner-protocol"; }
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java
new file mode 100644
index 0000000..4589794
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java
@@ -0,0 +1,77 @@
+/*
+ * 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.radius.form;
+
+import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
+
+/**
+ * Stores the RADIUS challenge message and expected credentials in a single
+ * object.
+ */
+public class GuacamoleRadiusChallenge {
+
+ /**
+ * The challenge text sent by the RADIUS server.
+ */
+ private final String challengeText;
+
+ /**
+ * The expected credentials that need to be provided to satisfy the
+ * RADIUS authentication challenge.
+ */
+ private final CredentialsInfo expectedCredentials;
+
+ /**
+ * Creates a new GuacamoleRadiusChallenge object with the provided
+ * challenge message and expected credentials.
+ *
+ * @param challengeText
+ * The challenge message sent by the RADIUS server.
+ *
+ * @param expectedCredentials
+ * The credentials required to complete the challenge.
+ */
+ public GuacamoleRadiusChallenge(String challengeText,
+ CredentialsInfo expectedCredentials) {
+ this.challengeText = challengeText;
+ this.expectedCredentials = expectedCredentials;
+ }
+
+ /**
+ * Returns the challenge message provided by the RADIUS server.
+ *
+ * @return
+ * The challenge message provided by the RADIUS server.
+ */
+ public String getChallengeText() {
+ return challengeText;
+ }
+
+ /**
+ * Returns the credentials required to satisfy the RADIUS challenge.
+ *
+ * @return
+ * The credentials required to satisfy the RADIUS challenge.
+ */
+ public CredentialsInfo getExpectedCredentials() {
+ return expectedCredentials;
+ }
+
+}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java
deleted file mode 100644
index 32ceb90..0000000
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * 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.radius.form;
-
-import org.apache.guacamole.form.Field;
-
-/**
- * A form used to prompt the user for additional information when
- * the RADIUS server sends a challenge back to the user with a reply
- * message.
- */
-public class RadiusChallengeResponseField extends Field {
-
- /**
- * The field returned by the RADIUS challenge/response.
- */
- public static final String PARAMETER_NAME = "guac-radius-challenge-response";
-
- /**
- * The type of field to initialize for the challenge/response.
- */
- private static final String RADIUS_FIELD_TYPE = "GUAC_RADIUS_CHALLENGE_RESPONSE";
-
- /**
- * The message the RADIUS server sent back in the challenge.
- */
- private final String challenge;
-
- /**
- * Initialize the field with the challenge sent back by the RADIUS server.
- *
- * @param challenge
- * The challenge message sent back by the RADIUS server.
- */
- public RadiusChallengeResponseField(String challenge) {
- super(PARAMETER_NAME, RADIUS_FIELD_TYPE);
- this.challenge = challenge;
-
- }
-
- /**
- * Get the challenge sent by the RADIUS server.
- *
- * @return
- * A String that indicates the challenge returned
- * by the RADIUS server.
- */
- public String getChallenge() {
- return challenge;
- }
-}
diff --git a/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js b/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
index dab0ffc..a3d72bf 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
+++ b/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
@@ -23,13 +23,6 @@
angular.module('guacRadius').config(['formServiceProvider',
function guacRadiusConfig(formServiceProvider) {
- // Define field for the challenge from the RADIUS service
- formServiceProvider.registerFieldType('GUAC_RADIUS_CHALLENGE_RESPONSE', {
- module : 'guacRadius',
- controller : 'radiusResponseController',
- templateUrl : 'app/ext/radius/templates/radiusResponseField.html'
- });
-
// Define the hidden field for the RADIUS state
formServiceProvider.registerFieldType('GUAC_RADIUS_STATE', {
module : 'guacRadius',
diff --git a/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js b/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js
deleted file mode 100644
index 4782b20..0000000
--- a/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * 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 the "GUAC_RADIUS_CHALLENGE_RESPONSE" field which
- * passes the RADIUS server challenge to the user and takes the response.
- */
-angular.module('guacRadius').controller('radiusResponseController', ['$scope', '$injector',
- function radiusResponseController($scope, $injector) {
-
- // Populate the reply message field
- $scope.radiusPlaceholder = $scope.field.challenge;
-
-}]);
diff --git a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
index 6e8e078..707f233 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
@@ -19,7 +19,6 @@
],
"resources" : {
- "templates/radiusResponseField.html" : "text/html",
"templates/radiusStateField.html" : "text/html"
}
diff --git a/extensions/guacamole-auth-radius/src/main/resources/templates/radiusResponseField.html b/extensions/guacamole-auth-radius/src/main/resources/templates/radiusResponseField.html
deleted file mode 100644
index eec760f..0000000
--- a/extensions/guacamole-auth-radius/src/main/resources/templates/radiusResponseField.html
+++ /dev/null
@@ -1 +0,0 @@
-<input type="password" ng-model="model" ng-trim="false" autocorrect="off" autocapitalize="off" placeholder="{{radiusPlaceholder}}" />
diff --git a/extensions/guacamole-auth-radius/src/main/resources/translations/en.json b/extensions/guacamole-auth-radius/src/main/resources/translations/en.json
index c068a70..1ba2623 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/translations/en.json
+++ b/extensions/guacamole-auth-radius/src/main/resources/translations/en.json
@@ -5,9 +5,8 @@
},
"LOGIN" : {
- "FIELD_HEADER_GUAC_RADIUS_CHALLENGE_RESPONSE" : "",
"FIELD_HEADER_GUAC_RADIUS_STATE" : "",
- "INFO_RADIUS_ADDL_REQUIRED" : "Please supply additional credentials"
+ "FIELD_HEADER_RADIUSCHALLENGE" : ""
}
}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java b/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java
index 1b45c95..b1d4865 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java
@@ -53,7 +53,7 @@
* Array of all known protocol names.
*/
private static final String[] KNOWN_PROTOCOLS = new String[]{
- "vnc", "rdp", "ssh", "telnet"};
+ "vnc", "rdp", "ssh", "telnet", "kubernetes"};
/**
* The hostname to use when connecting to guacd if no hostname is provided
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java b/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java
index 9fe76a4..d35facd 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java
@@ -117,6 +117,12 @@
*/
public static String QUERY_PARAMETER = "QUERY_PARAMETER";
+ /**
+ * A color scheme accepted by the Guacamole server terminal emulator
+ * and protocols which leverage it.
+ */
+ public static String TERMINAL_COLOR_SCHEME = "TERMINAL_COLOR_SCHEME";
+
}
/**
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/form/TerminalColorSchemeField.java b/guacamole-ext/src/main/java/org/apache/guacamole/form/TerminalColorSchemeField.java
new file mode 100644
index 0000000..ccd7ce5
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/form/TerminalColorSchemeField.java
@@ -0,0 +1,39 @@
+/*
+ * 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.form;
+
+/**
+ * Represents a terminal color scheme field. The field may contain only valid
+ * terminal color schemes as used by the Guacamole server terminal emulator
+ * and protocols which leverage it (SSH, telnet, Kubernetes).
+ */
+public class TerminalColorSchemeField extends Field {
+
+ /**
+ * Creates a new TerminalColorSchemeField with the given name.
+ *
+ * @param name
+ * The unique name to associate with this field.
+ */
+ public TerminalColorSchemeField(String name) {
+ super(name, Field.Type.TERMINAL_COLOR_SCHEME);
+ }
+
+}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java
index ad1d6d3..9bcce3e 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java
@@ -20,13 +20,18 @@
package org.apache.guacamole.net.auth;
import java.util.Date;
+import java.util.Map;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.net.GuacamoleTunnel;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
/**
* A pairing of username and GuacamoleTunnel representing an active usage of a
* particular connection.
*/
-public interface ActiveConnection extends Identifiable, Shareable<SharingProfile> {
+public interface ActiveConnection extends Identifiable, Connectable,
+ Shareable<SharingProfile> {
/**
* Returns the identifier of the connection being actively used. Unlike the
@@ -136,5 +141,31 @@
* The connected GuacamoleTunnel, or null if permission is denied.
*/
void setTunnel(GuacamoleTunnel tunnel);
+
+ /**
+ * Returns whether this ActiveConnection may be joined through a call to
+ * {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation, java.util.Map)}
+ * by the user that retrieved this ActiveConnection.
+ *
+ * @return
+ * true if the user that retrieved this ActiveConnection may join the
+ * ActiveConnection through a call to
+ * {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation, java.util.Map)},
+ * false otherwise.
+ */
+ default boolean isConnectable() {
+ return false;
+ }
+
+ @Override
+ default GuacamoleTunnel connect(GuacamoleClientInformation info,
+ Map<String, String> tokens) throws GuacamoleException {
+ throw new GuacamoleSecurityException("Permission denied.");
+ }
+
+ @Override
+ default int getActiveConnections() {
+ return 0;
+ }
}
diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/kubernetes.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/kubernetes.json
new file mode 100644
index 0000000..17df48a
--- /dev/null
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/kubernetes.json
@@ -0,0 +1,174 @@
+{
+ "name" : "kubernetes",
+
+ "connectionForms" : [
+
+ {
+ "name" : "network",
+ "fields" : [
+ {
+ "name" : "hostname",
+ "type" : "TEXT"
+ },
+ {
+ "name" : "port",
+ "type" : "NUMERIC"
+ },
+ {
+ "name" : "use-ssl",
+ "type" : "BOOLEAN",
+ "options" : [ "true" ]
+ },
+ {
+ "name" : "ignore-cert",
+ "type" : "BOOLEAN",
+ "options" : [ "true" ]
+ },
+ {
+ "name" : "ca-cert",
+ "type" : "MULTILINE"
+ }
+ ]
+ },
+
+ {
+ "name" : "container",
+ "fields" : [
+ {
+ "name" : "namespace",
+ "type" : "TEXT"
+ },
+ {
+ "name" : "pod",
+ "type" : "TEXT"
+ },
+ {
+ "name" : "container",
+ "type" : "TEXT"
+ }
+ ]
+ },
+
+ {
+ "name" : "authentication",
+ "fields" : [
+ {
+ "name" : "client-cert",
+ "type" : "MULTILINE"
+ },
+ {
+ "name" : "client-key",
+ "type" : "MULTILINE"
+ }
+ ]
+ },
+
+ {
+ "name" : "display",
+ "fields" : [
+ {
+ "name" : "color-scheme",
+ "type" : "TERMINAL_COLOR_SCHEME",
+ "options" : [ "", "black-white", "gray-black", "green-black", "white-black" ]
+ },
+ {
+ "name" : "font-name",
+ "type" : "TEXT"
+ },
+ {
+ "name" : "font-size",
+ "type" : "ENUM",
+ "options" : [ "", "8", "9", "10", "11", "12", "14", "18", "24", "30", "36", "48", "60", "72", "96" ]
+ },
+ {
+ "name" : "scrollback",
+ "type" : "NUMERIC"
+ },
+ {
+ "name" : "read-only",
+ "type" : "BOOLEAN",
+ "options" : [ "true" ]
+ }
+ ]
+ },
+
+ {
+ "name" : "behavior",
+ "fields" : [
+ {
+ "name" : "backspace",
+ "type" : "ENUM",
+ "options" : [ "", "127", "8" ]
+ }
+ ]
+ },
+
+ {
+ "name" : "typescript",
+ "fields" : [
+ {
+ "name" : "typescript-path",
+ "type" : "TEXT"
+ },
+ {
+ "name" : "typescript-name",
+ "type" : "TEXT"
+ },
+ {
+ "name" : "create-typescript-path",
+ "type" : "BOOLEAN",
+ "options" : [ "true" ]
+ }
+ ]
+ },
+
+ {
+ "name" : "recording",
+ "fields" : [
+ {
+ "name" : "recording-path",
+ "type" : "TEXT"
+ },
+ {
+ "name" : "recording-name",
+ "type" : "TEXT"
+ },
+ {
+ "name" : "recording-exclude-output",
+ "type" : "BOOLEAN",
+ "options" : [ "true" ]
+ },
+ {
+ "name" : "recording-exclude-mouse",
+ "type" : "BOOLEAN",
+ "options" : [ "true" ]
+ },
+ {
+ "name" : "recording-include-keys",
+ "type" : "BOOLEAN",
+ "options" : [ "true" ]
+ },
+ {
+ "name" : "create-recording-path",
+ "type" : "BOOLEAN",
+ "options" : [ "true" ]
+ }
+ ]
+ }
+
+ ],
+ "sharingProfileForms" : [
+
+ {
+ "name" : "display",
+ "fields" : [
+ {
+ "name" : "read-only",
+ "type" : "BOOLEAN",
+ "options" : [ "true" ]
+ }
+ ]
+ }
+
+ ]
+}
diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json
index 25537da..a1d01ed 100644
--- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json
@@ -47,7 +47,7 @@
"fields" : [
{
"name" : "color-scheme",
- "type" : "TEXT",
+ "type" : "TERMINAL_COLOR_SCHEME",
"options" : [ "", "black-white", "gray-black", "green-black", "white-black" ]
},
{
diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/telnet.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/telnet.json
index 02f9a20..0f70f05 100644
--- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/telnet.json
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/telnet.json
@@ -51,7 +51,7 @@
"fields" : [
{
"name" : "color-scheme",
- "type" : "TEXT",
+ "type" : "TERMINAL_COLOR_SCHEME",
"options" : [ "", "black-white", "gray-black", "green-black", "white-black" ]
},
{
diff --git a/guacamole/pom.xml b/guacamole/pom.xml
index 5552e4b..d7cf465 100644
--- a/guacamole/pom.xml
+++ b/guacamole/pom.xml
@@ -508,6 +508,13 @@
<version>1.0.10</version>
</dependency>
+ <!-- Pickr (JavaScript color picker) -->
+ <dependency>
+ <groupId>org.webjars.npm</groupId>
+ <artifactId>simonwep__pickr</artifactId>
+ <version>1.2.6</version>
+ </dependency>
+
</dependencies>
</project>
diff --git a/guacamole/src/licenses/LICENSE b/guacamole/src/licenses/LICENSE
index c3d308d..2995811 100644
--- a/guacamole/src/licenses/LICENSE
+++ b/guacamole/src/licenses/LICENSE
@@ -697,6 +697,37 @@
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+Pickr (https://simonwep.github.io/pickr/)
+-----------------------------------------
+
+ Version: 1.2.6
+ From: 'Simon Reinisch' (https://github.com/Simonwep/)
+ License(s):
+ MIT (bundled/pickr-1.2.6/LICENSE)
+
+MIT License
+
+Copyright (c) 2019 Simon Reinisch
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
Simple Logging Facade for Java (http://slf4j.org/)
--------------------------------------------------
diff --git a/guacamole/src/licenses/bundled/pickr-1.2.6/LICENSE b/guacamole/src/licenses/bundled/pickr-1.2.6/LICENSE
new file mode 100644
index 0000000..e02b384
--- /dev/null
+++ b/guacamole/src/licenses/bundled/pickr-1.2.6/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Simon Reinisch
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java b/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java
index 0041a03..1634378 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java
@@ -55,6 +55,11 @@
private final String username;
/**
+ * Whether this active connection may be connected to.
+ */
+ private final boolean connectable;
+
+ /**
* Creates a new APIActiveConnection, copying the information from the given
* active connection.
*
@@ -67,6 +72,7 @@
this.startDate = connection.getStartDate();
this.remoteHost = connection.getRemoteHost();
this.username = connection.getUsername();
+ this.connectable = connection.isConnectable();
}
/**
@@ -121,5 +127,16 @@
public String getIdentifier() {
return identifier;
}
-
+
+ /***
+ * Returns whether this active connection may be connected to, just as a
+ * normal connection.
+ *
+ * @return
+ * true if this active connection may be connected to, false otherwise.
+ */
+ public boolean isConnectable() {
+ return connectable;
+ }
+
}
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
index 7f38857..b168514 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java
@@ -36,6 +36,7 @@
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException;
+import org.apache.guacamole.net.auth.credentials.GuacamoleInsufficientCredentialsException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.apache.guacamole.net.event.AuthenticationFailureEvent;
import org.apache.guacamole.net.event.AuthenticationSuccessEvent;
@@ -170,7 +171,13 @@
return authenticatedUser;
}
- // First failure takes priority for now
+ // Insufficient credentials should take precedence
+ catch (GuacamoleInsufficientCredentialsException e) {
+ if (authFailure == null || authFailure instanceof GuacamoleInvalidCredentialsException)
+ authFailure = e;
+ }
+
+ // Catch other credentials exceptions and assign the first one
catch (GuacamoleCredentialsException e) {
if (authFailure == null)
authFailure = e;
diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequest.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequest.java
index 74e3b4d..fe58e18 100644
--- a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequest.java
+++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequest.java
@@ -21,8 +21,6 @@
import java.util.List;
import org.apache.guacamole.GuacamoleClientException;
-import org.apache.guacamole.GuacamoleClientException;
-import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleException;
/**
@@ -102,40 +100,6 @@
public static final String TIMEZONE_PARAMETER = "GUAC_TIMEZONE";
/**
- * All supported object types that can be used as the destination of a
- * tunnel.
- */
- public static enum Type {
-
- /**
- * A Guacamole connection.
- */
- CONNECTION("c"),
-
- /**
- * A Guacamole connection group.
- */
- CONNECTION_GROUP("g");
-
- /**
- * The parameter value which denotes a destination object of this type.
- */
- final String PARAMETER_VALUE;
-
- /**
- * Defines a Type having the given corresponding parameter value.
- *
- * @param value
- * The parameter value which denotes a destination object of this
- * type.
- */
- Type(String value) {
- PARAMETER_VALUE = value;
- }
-
- };
-
- /**
* Returns the value of the parameter having the given name.
*
* @param name
@@ -257,18 +221,11 @@
* If the type was not present in the request, or if the type requested
* is in the wrong format.
*/
- public Type getType() throws GuacamoleException {
+ public TunnelRequestType getType() throws GuacamoleException {
- String type = getRequiredParameter(TYPE_PARAMETER);
-
- // For each possible object type
- for (Type possibleType : Type.values()) {
-
- // Match against defined parameter value
- if (type.equals(possibleType.PARAMETER_VALUE))
- return possibleType;
-
- }
+ TunnelRequestType type = TunnelRequestType.parseType(getRequiredParameter(TYPE_PARAMETER));
+ if (type != null)
+ return type;
throw new GuacamoleClientException("Illegal identifier - unknown type.");
diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java
index 598a4e5..fa56b19 100644
--- a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java
+++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java
@@ -24,15 +24,13 @@
import java.util.List;
import java.util.Map;
import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.GuacamoleSecurityException;
+import org.apache.guacamole.GuacamoleResourceNotFoundException;
import org.apache.guacamole.GuacamoleSession;
import org.apache.guacamole.GuacamoleUnauthorizedException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.AuthenticatedUser;
-import org.apache.guacamole.net.auth.Connection;
-import org.apache.guacamole.net.auth.ConnectionGroup;
+import org.apache.guacamole.net.auth.Connectable;
import org.apache.guacamole.net.auth.Credentials;
-import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.event.TunnelCloseEvent;
import org.apache.guacamole.net.event.TunnelConnectEvent;
@@ -204,58 +202,20 @@
* If an error occurs while creating the tunnel.
*/
protected GuacamoleTunnel createConnectedTunnel(UserContext context,
- final TunnelRequest.Type type, String id,
+ final TunnelRequestType type, String id,
GuacamoleClientInformation info, Map<String, String> tokens)
throws GuacamoleException {
- // Create connected tunnel from identifier
- GuacamoleTunnel tunnel = null;
- switch (type) {
+ // Retrieve requested destination object
+ Connectable connectable = type.getConnectable(context, id);
+ if (connectable == null)
+ throw new GuacamoleResourceNotFoundException("Requested tunnel "
+ + "destination does not exist.");
- // Connection identifiers
- case CONNECTION: {
-
- // Get connection directory
- Directory<Connection> directory = context.getConnectionDirectory();
-
- // Get authorized connection
- Connection connection = directory.get(id);
- if (connection == null) {
- logger.info("Connection \"{}\" does not exist for user \"{}\".", id, context.self().getIdentifier());
- throw new GuacamoleSecurityException("Requested connection is not authorized.");
- }
-
- // Connect tunnel
- tunnel = connection.connect(info, tokens);
- logger.info("User \"{}\" connected to connection \"{}\".", context.self().getIdentifier(), id);
- break;
- }
-
- // Connection group identifiers
- case CONNECTION_GROUP: {
-
- // Get connection group directory
- Directory<ConnectionGroup> directory = context.getConnectionGroupDirectory();
-
- // Get authorized connection group
- ConnectionGroup group = directory.get(id);
- if (group == null) {
- logger.info("Connection group \"{}\" does not exist for user \"{}\".", id, context.self().getIdentifier());
- throw new GuacamoleSecurityException("Requested connection group is not authorized.");
- }
-
- // Connect tunnel
- tunnel = group.connect(info, tokens);
- logger.info("User \"{}\" connected to group \"{}\".", context.self().getIdentifier(), id);
- break;
- }
-
- // Type is guaranteed to be one of the above
- default:
- assert(false);
-
- }
-
+ // Connect tunnel to destination
+ GuacamoleTunnel tunnel = connectable.connect(info, tokens);
+ logger.info("User \"{}\" connected to {} \"{}\".",
+ context.self().getIdentifier(), type.NAME, id);
return tunnel;
}
@@ -297,7 +257,7 @@
*/
protected GuacamoleTunnel createAssociatedTunnel(final GuacamoleTunnel tunnel,
final String authToken, final GuacamoleSession session,
- final UserContext context, final TunnelRequest.Type type,
+ final UserContext context, final TunnelRequestType type,
final String id) throws GuacamoleException {
// Monitor tunnel closure and data
@@ -320,26 +280,9 @@
long connectionEndTime = System.currentTimeMillis();
long duration = connectionEndTime - connectionStartTime;
- // Log closure
- switch (type) {
-
- // Connection identifiers
- case CONNECTION:
- logger.info("User \"{}\" disconnected from connection \"{}\". Duration: {} milliseconds",
- session.getAuthenticatedUser().getIdentifier(), id, duration);
- break;
-
- // Connection group identifiers
- case CONNECTION_GROUP:
- logger.info("User \"{}\" disconnected from connection group \"{}\". Duration: {} milliseconds",
- session.getAuthenticatedUser().getIdentifier(), id, duration);
- break;
-
- // Type is guaranteed to be one of the above
- default:
- assert(false);
-
- }
+ logger.info("User \"{}\" disconnected from {} \"{}\". Duration: {} milliseconds",
+ session.getAuthenticatedUser().getIdentifier(),
+ type.NAME, id, duration);
try {
@@ -390,7 +333,7 @@
// Parse request parameters
String authToken = request.getAuthenticationToken();
String id = request.getIdentifier();
- TunnelRequest.Type type = request.getType();
+ TunnelRequestType type = request.getType();
String authProviderIdentifier = request.getAuthenticationProviderIdentifier();
GuacamoleClientInformation info = getClientInformation(request);
diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestType.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestType.java
new file mode 100644
index 0000000..2c98708
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestType.java
@@ -0,0 +1,154 @@
+/*
+ * 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.tunnel;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.ActiveConnection;
+import org.apache.guacamole.net.auth.Connectable;
+import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.ConnectionGroup;
+import org.apache.guacamole.net.auth.UserContext;
+
+/**
+ * All supported object types that can be used as the destination of a tunnel.
+ *
+ * @see TunnelRequest#TYPE_PARAMETER
+ * @see TunnelRequest#getType()
+ */
+public enum TunnelRequestType {
+
+ /**
+ * A Guacamole connection.
+ */
+ CONNECTION("c", "connection") {
+
+ @Override
+ public Connection getConnectable(UserContext userContext,
+ String identifier) throws GuacamoleException {
+ return userContext.getConnectionDirectory().get(identifier);
+ }
+
+ },
+
+ /**
+ * A Guacamole connection group.
+ */
+ CONNECTION_GROUP("g", "connection group") {
+
+ @Override
+ public ConnectionGroup getConnectable(UserContext userContext,
+ String identifier) throws GuacamoleException {
+ return userContext.getConnectionGroupDirectory().get(identifier);
+ }
+
+ },
+
+ /**
+ * An active Guacamole connection.
+ */
+ ACTIVE_CONNECTION("a", "active connection") {
+
+ @Override
+ public ActiveConnection getConnectable(UserContext userContext,
+ String identifier) throws GuacamoleException {
+ return userContext.getActiveConnectionDirectory().get(identifier);
+ }
+
+ };
+
+ /**
+ * The parameter value which denotes a destination object of this type
+ * within a tunnel request.
+ *
+ * @see TunnelRequest#TYPE_PARAMETER
+ * @see TunnelRequest#getType()
+ */
+ public final String PARAMETER_VALUE;
+
+ /**
+ * A human-readable, descriptive name of the type of destination object.
+ */
+ public final String NAME;
+
+ /**
+ * Defines a tunnel request type having the given corresponding parameter
+ * value and human-readable name.
+ *
+ * @param value
+ * The parameter value which denotes a destination object of this
+ * type.
+ *
+ * @param name
+ * A human-readable, descriptive name of the type of destination
+ * object.
+ */
+ private TunnelRequestType(String value, String name) {
+ PARAMETER_VALUE = value;
+ NAME = name;
+ }
+
+ /**
+ * Retrieves the object having the given identifier from the given
+ * UserContext, where the type of object retrieved is the type of object
+ * represented by this tunnel request type.
+ *
+ * @param userContext
+ * The UserContext to retrieve the object from.
+ *
+ * @param identifier
+ * The identifier of the object to retrieve.
+ *
+ * @return
+ * The object having the given identifier, or null if no such object
+ * exists.
+ *
+ * @throws GuacamoleException
+ * If an error occurs retrieving the requested object, or if permission
+ * to retrieve the object is denied.
+ */
+ public abstract Connectable getConnectable(UserContext userContext,
+ String identifier) throws GuacamoleException;
+
+ /**
+ * Parses the given tunnel request type string, returning the
+ * TunnelRequestType which matches that string, as declared by
+ * {@link #PARAMETER_VALUE}. If no such type exists, null is returned.
+ *
+ * @param type
+ * The type string to parse.
+ *
+ * @return
+ * The TunnelRequestType which specifies the given string as its
+ * {@link #PARAMETER_VALUE}, or null if no such type exists.
+ */
+ public static TunnelRequestType parseType(String type) {
+
+ // Locate type with given parameter value
+ for (TunnelRequestType possibleType : values()) {
+ if (type.equals(possibleType.PARAMETER_VALUE))
+ return possibleType;
+ }
+
+ // No such type
+ return null;
+
+ }
+
+}
diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js
index f787918..c8941b3 100644
--- a/guacamole/src/main/webapp/app/client/controllers/clientController.js
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -28,6 +28,7 @@
var ManagedClient = $injector.get('ManagedClient');
var ManagedClientState = $injector.get('ManagedClientState');
var ManagedFilesystem = $injector.get('ManagedFilesystem');
+ var Protocol = $injector.get('Protocol');
var ScrollState = $injector.get('ScrollState');
// Required services
@@ -251,7 +252,15 @@
*
* @type ScrollState
*/
- scrollState : new ScrollState()
+ scrollState : new ScrollState(),
+
+ /**
+ * The current desired values of all editable connection parameters as
+ * a set of name/value pairs, including any changes made by the user.
+ *
+ * @type {Object.<String, String>}
+ */
+ connectionParameters : {}
};
@@ -261,6 +270,16 @@
};
/**
+ * Applies any changes to connection parameters made by the user within the
+ * Guacamole menu.
+ */
+ $scope.applyParameterChanges = function applyParameterChanges() {
+ angular.forEach($scope.menu.connectionParameters, function sendArgv(value, name) {
+ ManagedClient.setArgument($scope.client, name, value);
+ });
+ };
+
+ /**
* The client which should be attached to the client UI.
*
* @type ManagedClient
@@ -490,12 +509,20 @@
});
+ // Update client state/behavior as visibility of the Guacamole menu changes
$scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) {
- // Send clipboard data if menu is hidden
- if (!menuShown && menuShownPreviousState)
+ // Send clipboard and argument value data once menu is hidden
+ if (!menuShown && menuShownPreviousState) {
$scope.$broadcast('guacClipboard', $scope.client.clipboardData);
-
+ $scope.applyParameterChanges();
+ }
+
+ // Obtain snapshot of current editable connection parameters when menu
+ // is opened
+ else if (menuShown)
+ $scope.menu.connectionParameters = ManagedClient.getArgumentModel($scope.client);
+
// Disable client keyboard if the menu is shown
$scope.client.clientProperties.keyboardEnabled = !menuShown;
@@ -873,6 +900,11 @@
$scope.clientMenuActions = [ DISCONNECT_MENU_ACTION ];
/**
+ * @borrows Protocol.getNamespace
+ */
+ $scope.getProtocolNamespace = Protocol.getNamespace;
+
+ /**
* The currently-visible filesystem within the filesystem menu, if the
* filesystem menu is open. If no filesystem is currently visible, this
* will be null.
diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html
index 3f56a9d..5325b47 100644
--- a/guacamole/src/main/webapp/app/client/templates/client.html
+++ b/guacamole/src/main/webapp/app/client/templates/client.html
@@ -119,6 +119,14 @@
</div>
</div>
+ <!-- Connection parameters which may be modified while the connection is open -->
+ <div class="menu-section connection-parameters" id="connection-settings" ng-show="client.protocol">
+ <guac-form namespace="getProtocolNamespace(client.protocol)"
+ content="client.forms"
+ model="menu.connectionParameters"
+ model-only="true"></guac-form>
+ </div>
+
<!-- Input method -->
<div class="menu-section" id="keyboard-settings">
<h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3>
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedArgument.js b/guacamole/src/main/webapp/app/client/types/ManagedArgument.js
new file mode 100644
index 0000000..247d9f6
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/types/ManagedArgument.js
@@ -0,0 +1,152 @@
+/*
+ * 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.
+ */
+
+/**
+ * Provides the ManagedArgument class used by ManagedClient.
+ */
+angular.module('client').factory('ManagedArgument', ['$q', function defineManagedArgument($q) {
+
+ /**
+ * Object which represents an argument (connection parameter) which may be
+ * changed by the user while the connection is open.
+ *
+ * @constructor
+ * @param {ManagedArgument|Object} [template={}]
+ * The object whose properties should be copied within the new
+ * ManagedArgument.
+ */
+ var ManagedArgument = function ManagedArgument(template) {
+
+ // Use empty object by default
+ template = template || {};
+
+ /**
+ * The name of the connection parameter.
+ *
+ * @type {String}
+ */
+ this.name = template.name;
+
+ /**
+ * The current value of the connection parameter.
+ *
+ * @type {String}
+ */
+ this.value = template.value;
+
+ /**
+ * A valid, open output stream which may be used to apply a new value
+ * to the connection parameter.
+ *
+ * @type {Guacamole.OutputStream}
+ */
+ this.stream = template.stream;
+
+ };
+
+ /**
+ * Requests editable access to a given connection parameter, returning a
+ * promise which is resolved with a ManagedArgument instance that provides
+ * such access if the parameter is indeed editable.
+ *
+ * @param {ManagedClient} managedClient
+ * The ManagedClient instance associated with the connection for which
+ * an editable version of the connection parameter is being retrieved.
+ *
+ * @param {String} name
+ * The name of the connection parameter.
+ *
+ * @param {String} value
+ * The current value of the connection parameter, as received from a
+ * prior, inbound "argv" stream.
+ *
+ * @returns {Promise.<ManagedArgument>}
+ * A promise which is resolved with the new ManagedArgument instance
+ * once the requested parameter has been verified as editable.
+ */
+ ManagedArgument.getInstance = function getInstance(managedClient, name, value) {
+
+ var deferred = $q.defer();
+
+ // Create internal, fully-populated instance of ManagedArgument, to be
+ // returned only once mutability of the associated connection parameter
+ // has been verified
+ var managedArgument = new ManagedArgument({
+ name : name,
+ value : value,
+ stream : managedClient.client.createArgumentValueStream('text/plain', name)
+ });
+
+ // The connection parameter is editable only if a successful "ack" is
+ // received
+ managedArgument.stream.onack = function ackReceived(status) {
+ if (status.isError())
+ deferred.reject(status);
+ else
+ deferred.resolve(managedArgument);
+ };
+
+ return deferred.promise;
+
+ };
+
+ /**
+ * Sets the given editable argument (connection parameter) to the given
+ * value, updating the behavior of the associated connection in real-time.
+ * If successful, the ManagedArgument provided cannot be used for future
+ * calls to setValue() and must be replaced with a new instance. This
+ * function only has an effect if the new parameter value is different from
+ * the current value.
+ *
+ * @param {ManagedArgument} managedArgument
+ * The ManagedArgument instance associated with the connection
+ * parameter being modified.
+ *
+ * @param {String} value
+ * The new value to assign to the connection parameter.
+ *
+ * @returns {Boolean}
+ * true if the connection parameter was sent and the provided
+ * ManagedArgument instance may no longer be used for future setValue()
+ * calls, false if the connection parameter was NOT sent as it has not
+ * changed.
+ */
+ ManagedArgument.setValue = function setValue(managedArgument, value) {
+
+ // Stream new value only if value has changed
+ if (value !== managedArgument.value) {
+
+ var writer = new Guacamole.StringWriter(managedArgument.stream);
+ writer.sendText(value);
+ writer.sendEnd();
+
+ // ManagedArgument instance is no longer usable
+ return true;
+
+ }
+
+ // No parameter value change was attempted and the ManagedArgument
+ // instance may be reused
+ return false;
+
+ };
+
+ return ManagedArgument;
+
+}]);
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
index de679d6..c1eccdd 100644
--- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js
+++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
@@ -27,6 +27,7 @@
var ClientProperties = $injector.get('ClientProperties');
var ClientIdentifier = $injector.get('ClientIdentifier');
var ClipboardData = $injector.get('ClipboardData');
+ var ManagedArgument = $injector.get('ManagedArgument');
var ManagedClientState = $injector.get('ManagedClientState');
var ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
var ManagedDisplay = $injector.get('ManagedDisplay');
@@ -35,20 +36,22 @@
var ManagedShareLink = $injector.get('ManagedShareLink');
// Required services
- var $document = $injector.get('$document');
- var $q = $injector.get('$q');
- var $rootScope = $injector.get('$rootScope');
- var $window = $injector.get('$window');
- var authenticationService = $injector.get('authenticationService');
- var connectionGroupService = $injector.get('connectionGroupService');
- var connectionService = $injector.get('connectionService');
- var preferenceService = $injector.get('preferenceService');
- var requestService = $injector.get('requestService');
- var tunnelService = $injector.get('tunnelService');
- var guacAudio = $injector.get('guacAudio');
- var guacHistory = $injector.get('guacHistory');
- var guacImage = $injector.get('guacImage');
- var guacVideo = $injector.get('guacVideo');
+ var $document = $injector.get('$document');
+ var $q = $injector.get('$q');
+ var $rootScope = $injector.get('$rootScope');
+ var $window = $injector.get('$window');
+ var activeConnectionService = $injector.get('activeConnectionService');
+ var authenticationService = $injector.get('authenticationService');
+ var connectionGroupService = $injector.get('connectionGroupService');
+ var connectionService = $injector.get('connectionService');
+ var preferenceService = $injector.get('preferenceService');
+ var requestService = $injector.get('requestService');
+ var schemaService = $injector.get('schemaService');
+ var tunnelService = $injector.get('tunnelService');
+ var guacAudio = $injector.get('guacAudio');
+ var guacHistory = $injector.get('guacHistory');
+ var guacImage = $injector.get('guacImage');
+ var guacVideo = $injector.get('guacVideo');
/**
* The minimum amount of time to wait between updates to the client
@@ -128,6 +131,23 @@
this.title = template.title;
/**
+ * The name which uniquely identifies the protocol of the connection in
+ * use. If the protocol cannot be determined, such as when a connection
+ * group is in use, this will be null.
+ *
+ * @type {String}
+ */
+ this.protocol = template.protocol || null;
+
+ /**
+ * An array of forms describing all known parameters for the connection
+ * in use, including those which may not be editable.
+ *
+ * @type {Form[]}
+ */
+ this.forms = template.forms || [];
+
+ /**
* The most recently-generated thumbnail for this connection, as
* stored within the local connection history. If no thumbnail is
* stored, this will be null.
@@ -189,6 +209,17 @@
*/
this.clientProperties = template.clientProperties || new ClientProperties();
+ /**
+ * All editable arguments (connection parameters), stored by their
+ * names. Arguments will only be present within this set if their
+ * current values have been exposed by the server via an inbound "argv"
+ * stream and the server has confirmed that the value may be changed
+ * through a successful "ack" to an outbound "argv" stream.
+ *
+ * @type {Object.<String, ManagedArgument>}
+ */
+ this.arguments = template.arguments || {};
+
};
/**
@@ -458,6 +489,33 @@
};
+ // Test for argument mutability whenever an argument value is
+ // received
+ client.onargv = function clientArgumentValueReceived(stream, mimetype, name) {
+
+ // Ignore arguments which do not use a mimetype currently supported
+ // by the web application
+ if (mimetype !== 'text/plain')
+ return;
+
+ var reader = new Guacamole.StringReader(stream);
+
+ // Assemble received data into a single string
+ var value = '';
+ reader.ontext = function textReceived(text) {
+ value += text;
+ };
+
+ // Test mutability once stream is finished, storing the current
+ // value for the argument only if it is mutable
+ reader.onend = function textComplete() {
+ ManagedArgument.getInstance(managedClient, name, value).then(function argumentIsMutable(argument) {
+ managedClient.arguments[name] = argument;
+ }, function ignoreImmutableArguments() {});
+ };
+
+ };
+
// Handle any received clipboard data
client.onclipboard = function clientClipboardReceived(stream, mimetype) {
@@ -532,11 +590,16 @@
client.connect(connectString);
});
- // If using a connection, pull connection name
+ // If using a connection, pull connection name and protocol information
if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) {
- connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id)
- .then(function connectionRetrieved(connection) {
- managedClient.name = managedClient.title = connection.name;
+ $q.all({
+ connection : connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id),
+ protocols : schemaService.getProtocols(clientIdentifier.dataSource)
+ })
+ .then(function dataRetrieved(values) {
+ managedClient.name = managedClient.title = values.connection.name;
+ managedClient.protocol = values.connection.protocol;
+ managedClient.forms = values.protocols[values.connection.protocol].connectionForms;
}, requestService.WARN);
}
@@ -548,6 +611,29 @@
}, requestService.WARN);
}
+ // If using an active connection, pull corresponding connection, then
+ // pull connection name and protocol information from that
+ else if (clientIdentifier.type === ClientIdentifier.Types.ACTIVE_CONNECTION) {
+ activeConnectionService.getActiveConnection(clientIdentifier.dataSource, clientIdentifier.id)
+ .then(function activeConnectionRetrieved(activeConnection) {
+
+ // Attempt to retrieve connection details only if the
+ // underlying connection is known
+ if (activeConnection.connectionIdentifier) {
+ $q.all({
+ connection : connectionService.getConnection(clientIdentifier.dataSource, activeConnection.connectionIdentifier),
+ protocols : schemaService.getProtocols(clientIdentifier.dataSource)
+ })
+ .then(function dataRetrieved(values) {
+ managedClient.name = managedClient.title = values.connection.name;
+ managedClient.protocol = values.connection.protocol;
+ managedClient.forms = values.protocols[values.connection.protocol].connectionForms;
+ }, requestService.WARN);
+ }
+
+ }, requestService.WARN);
+ }
+
return managedClient;
};
@@ -630,6 +716,52 @@
};
/**
+ * Assigns the given value to the connection parameter having the given
+ * name, updating the behavior of the connection in real-time. If the
+ * connection parameter is not editable, this function has no effect.
+ *
+ * @param {ManagedClient} managedClient
+ * The ManagedClient instance associated with the active connection
+ * being modified.
+ *
+ * @param {String} name
+ * The name of the connection parameter to modify.
+ *
+ * @param {String} value
+ * The value to attempt to assign to the given connection parameter.
+ */
+ ManagedClient.setArgument = function setArgument(managedClient, name, value) {
+ var managedArgument = managedClient.arguments[name];
+ if (managedArgument && ManagedArgument.setValue(managedArgument, value))
+ delete managedClient.arguments[name];
+ };
+
+ /**
+ * 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
+ * edits those parameters.
+ *
+ * @param {ManagedClient} client
+ * The ManagedClient instance associated with the active connection
+ * whose parameter values are being retrieved.
+ *
+ * @returns {Object.<String, String>}
+ * A new set of name/value pairs containing the current values of all
+ * editable parameters.
+ */
+ ManagedClient.getArgumentModel = function getArgumentModel(client) {
+
+ var model = {};
+
+ angular.forEach(client.arguments, function addModelEntry(managedArgument) {
+ model[managedArgument.name] = managedArgument.value;
+ });
+
+ return model;
+
+ };
+
+ /**
* Produces a sharing link for the given ManagedClient using the given
* sharing profile. The resulting sharing link, and any required login
* information, can be retrieved from the <code>shareLinks</code> property
diff --git a/guacamole/src/main/webapp/app/form/controllers/terminalColorSchemeFieldController.js b/guacamole/src/main/webapp/app/form/controllers/terminalColorSchemeFieldController.js
new file mode 100644
index 0000000..fb85a50
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/controllers/terminalColorSchemeFieldController.js
@@ -0,0 +1,142 @@
+/*
+ * 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 terminal color scheme fields.
+ */
+angular.module('form').controller('terminalColorSchemeFieldController', ['$scope', '$injector',
+ function terminalColorSchemeFieldController($scope, $injector) {
+
+ // Required types
+ var ColorScheme = $injector.get('ColorScheme');
+
+ /**
+ * The currently selected color scheme. If a pre-defined color scheme is
+ * selected, this will be the connection parameter value associated with
+ * that color scheme. If a custom color scheme is selected, this will be
+ * the string "custom".
+ *
+ * @type String
+ */
+ $scope.selectedColorScheme = '';
+
+ /**
+ * The current custom color scheme, if a custom color scheme has been
+ * specified. If no custom color scheme has yet been specified, this will
+ * be a ColorScheme instance that has been initialized to the default
+ * colors.
+ *
+ * @type ColorScheme
+ */
+ $scope.customColorScheme = new ColorScheme();
+
+ /**
+ * The array of colors to include within the color picker as pre-defined
+ * options for convenience.
+ *
+ * @type String[]
+ */
+ $scope.defaultPalette = new ColorScheme().colors;
+
+ /**
+ * Whether the raw details of the custom color scheme should be shown. By
+ * default, such details are hidden.
+ *
+ * @type Boolean
+ */
+ $scope.detailsShown = false;
+
+ /**
+ * The palette indices of all colors which are considered low-intensity.
+ *
+ * @type Number[]
+ */
+ $scope.lowIntensity = [ 0, 1, 2, 3, 4, 5, 6, 7 ];
+
+ /**
+ * The palette indices of all colors which are considered high-intensity.
+ *
+ * @type Number[]
+ */
+ $scope.highIntensity = [ 8, 9, 10, 11, 12, 13, 14, 15 ];
+
+ /**
+ * The string value which is assigned to selectedColorScheme if a custom
+ * color scheme is selected.
+ *
+ * @constant
+ * @type String
+ */
+ var CUSTOM_COLOR_SCHEME = 'custom';
+
+ /**
+ * Returns whether a custom color scheme has been selected.
+ *
+ * @returns {Boolean}
+ * true if a custom color scheme has been selected, false otherwise.
+ */
+ $scope.isCustom = function isCustom() {
+ return $scope.selectedColorScheme === CUSTOM_COLOR_SCHEME;
+ };
+
+ /**
+ * Shows the raw details of the custom color scheme. If the details are
+ * already shown, this function has no effect.
+ */
+ $scope.showDetails = function showDetails() {
+ $scope.detailsShown = true;
+ };
+
+ /**
+ * Hides the raw details of the custom color scheme. If the details are
+ * already hidden, this function has no effect.
+ */
+ $scope.hideDetails = function hideDetails() {
+ $scope.detailsShown = false;
+ };
+
+ // Keep selected color scheme and custom color scheme in sync with changes
+ // to model
+ $scope.$watch('model', function modelChanged(model) {
+ if ($scope.selectedColorScheme === CUSTOM_COLOR_SCHEME || (model && !_.includes($scope.field.options, model))) {
+ $scope.customColorScheme = ColorScheme.fromString(model);
+ $scope.selectedColorScheme = CUSTOM_COLOR_SCHEME;
+ }
+ else
+ $scope.selectedColorScheme = model || '';
+ });
+
+ // Keep model in sync with changes to selected color scheme
+ $scope.$watch('selectedColorScheme', function selectedColorSchemeChanged(selectedColorScheme) {
+ if (!selectedColorScheme)
+ $scope.model = '';
+ else if (selectedColorScheme === CUSTOM_COLOR_SCHEME)
+ $scope.model = ColorScheme.toString($scope.customColorScheme);
+ else
+ $scope.model = selectedColorScheme;
+ });
+
+ // Keep model in sync with changes to custom color scheme
+ $scope.$watch('customColorScheme', function customColorSchemeChanged(customColorScheme) {
+ if ($scope.selectedColorScheme === CUSTOM_COLOR_SCHEME)
+ $scope.model = ColorScheme.toString(customColorScheme);
+ }, true);
+
+}]);
diff --git a/guacamole/src/main/webapp/app/form/controllers/textFieldController.js b/guacamole/src/main/webapp/app/form/controllers/textFieldController.js
index b5bc753..8dd134a 100644
--- a/guacamole/src/main/webapp/app/form/controllers/textFieldController.js
+++ b/guacamole/src/main/webapp/app/form/controllers/textFieldController.js
@@ -35,6 +35,6 @@
// Generate unique ID for datalist, if applicable
if ($scope.field.options && $scope.field.options.length)
- $scope.dataListId = $scope.field.name + '-datalist';
+ $scope.dataListId = $scope.fieldId + '-datalist';
}]);
diff --git a/guacamole/src/main/webapp/app/form/directives/formField.js b/guacamole/src/main/webapp/app/form/directives/formField.js
index cf3efd0..fbc0cfe 100644
--- a/guacamole/src/main/webapp/app/form/directives/formField.js
+++ b/guacamole/src/main/webapp/app/form/directives/formField.js
@@ -88,6 +88,18 @@
var fieldContent = $element.find('.form-field');
/**
+ * An ID value which is reasonably likely to be unique relative to
+ * other elements on the page. This ID should be used to associate
+ * the relevant input element with the label provided by the
+ * guacFormField directive, if there is such an input element.
+ *
+ * @type String
+ */
+ $scope.fieldId = 'guac-field-XXXXXXXXXXXXXXXX'.replace(/X/g, function getRandomCharacter() {
+ return Math.floor(Math.random() * 36).toString(36);
+ }) + '-' + new Date().getTime().toString(36);
+
+ /**
* Produces the translation string for the header of the current
* field. The translation string will be of the form:
*
@@ -173,4 +185,4 @@
}] // end controller
};
-}]);
\ No newline at end of file
+}]);
diff --git a/guacamole/src/main/webapp/app/form/directives/guacInputColor.js b/guacamole/src/main/webapp/app/form/directives/guacInputColor.js
new file mode 100644
index 0000000..1762c31
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/directives/guacInputColor.js
@@ -0,0 +1,117 @@
+/*
+ * 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.
+ */
+
+/**
+ * A directive which implements a color input field. If the underlying color
+ * picker implementation cannot be used due to a lack of browser support, this
+ * directive will become read-only, functioning essentially as a color preview.
+ *
+ * @see colorPickerService
+ */
+angular.module('form').directive('guacInputColor', [function guacInputColor() {
+
+ var config = {
+ restrict: 'E',
+ replace: true,
+ templateUrl: 'app/form/templates/guacInputColor.html',
+ transclude: true
+ };
+
+ config.scope = {
+
+ /**
+ * The current selected color value, in standard 6-digit hexadecimal
+ * RGB notation. When the user selects a different color using this
+ * directive, this value will updated accordingly.
+ *
+ * @type String
+ */
+ model: '=',
+
+ /**
+ * An optional array of colors to include within the color picker as a
+ * convenient selection of pre-defined colors. The colors within the
+ * array must be in standard 6-digit hexadecimal RGB notation.
+ *
+ * @type String[]
+ */
+ palette: '='
+
+ };
+
+ config.controller = ['$scope', '$element', '$injector',
+ function guacInputColorController($scope, $element, $injector) {
+
+ // Required services
+ var colorPickerService = $injector.get('colorPickerService');
+
+ /**
+ * @borrows colorPickerService.isAvailable()
+ */
+ $scope.isColorPickerAvailable = colorPickerService.isAvailable;
+
+ /**
+ * Returns whether the color currently selected is "dark" in the sense
+ * that the color white will have higher contrast against it than the
+ * color black.
+ *
+ * @returns {Boolean}
+ * true if the currently selected color is relatively dark (white
+ * text would provide better contrast than black), false otherwise.
+ */
+ $scope.isDark = function isDark() {
+
+ // Assume not dark if color is invalid or undefined
+ var rgb = $scope.model && /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec($scope.model);
+ if (!rgb)
+ return false;
+
+ // Parse color component values as hexadecimal
+ var red = parseInt(rgb[1], 16);
+ var green = parseInt(rgb[2], 16);
+ var blue = parseInt(rgb[3], 16);
+
+ // Convert RGB to luminance in HSL space (as defined by the
+ // relative luminance formula given by the W3C for accessibility)
+ var luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
+
+ // Consider the background to be dark if white text over that
+ // background would provide better contrast than black
+ return luminance <= 153; // 153 is the component value 0.6 converted from 0-1 to the 0-255 range
+
+ };
+
+ /**
+ * Prompts the user to choose a color by displaying a color selection
+ * dialog. If the user chooses a color, this directive's model is
+ * automatically updated. If the user cancels the dialog, the model is
+ * left untouched.
+ */
+ $scope.selectColor = function selectColor() {
+ colorPickerService.selectColor($element[0], $scope.model, $scope.palette)
+ .then(function colorSelected(color) {
+ $scope.model = color;
+ }, angular.noop);
+ };
+
+ }];
+
+ return config;
+
+}]);
diff --git a/guacamole/src/main/webapp/app/form/services/colorPickerService.js b/guacamole/src/main/webapp/app/form/services/colorPickerService.js
new file mode 100644
index 0000000..cb9e63f
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/services/colorPickerService.js
@@ -0,0 +1,268 @@
+/*
+ * 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.
+ */
+
+/**
+ * A service for prompting the user to choose a color using the "Pickr" color
+ * picker. As the Pickr color picker might not be available if the JavaScript
+ * features it requires are not supported by the browser (Internet Explorer),
+ * the isAvailable() function should be used to test for usability.
+ */
+angular.module('form').provider('colorPickerService', function colorPickerServiceProvider() {
+
+ /**
+ * A singleton instance of the "Pickr" color picker, shared by all users of
+ * this service. Pickr does not initialize synchronously, nor is it
+ * supported by all browsers. If Pickr is not yet initialized, or is
+ * unsupported, this will be null.
+ *
+ * @type {Pickr}
+ */
+ var pickr = null;
+
+ /**
+ * Whether Pickr has completed initialization.
+ *
+ * @type {Boolean}
+ */
+ var pickrInitComplete = false;
+
+ /**
+ * The HTML element to provide to Pickr as the root element.
+ *
+ * @type {HTMLDivElement}
+ */
+ var pickerContainer = document.createElement('div');
+ pickerContainer.className = 'shared-color-picker';
+
+ /**
+ * An instance of Deferred which represents an active request for the
+ * user to choose a color. The promise associated with the Deferred will
+ * be resolved with the chosen color once a color is chosen, and rejected
+ * if the request is cancelled or Pickr is not available. If no request is
+ * active, this will be null.
+ *
+ * @type {Deferred}
+ */
+ var activeRequest = null;
+
+ /**
+ * Resolves the current active request with the given color value. If no
+ * color value is provided, the active request is rejected. If no request
+ * is active, this function has no effect.
+ *
+ * @param {String} [color]
+ * The color value to resolve the active request with.
+ */
+ var completeActiveRequest = function completeActiveRequest(color) {
+ if (activeRequest) {
+
+ // Hide color picker, if shown
+ pickr.hide();
+
+ // Resolve/reject active request depending on value provided
+ if (color)
+ activeRequest.resolve(color);
+ else
+ activeRequest.reject();
+
+ // No active request
+ activeRequest = null;
+
+ }
+ };
+
+ try {
+ pickr = Pickr.create({
+
+ // Bind color picker to the container element
+ el : pickerContainer,
+
+ // Wrap color picker dialog in Guacamole-specific class for
+ // sake of additional styling
+ appClass : 'guac-input-color-picker',
+
+ 'default' : '#000000',
+
+ // Display color details as hex
+ defaultRepresentation : 'HEX',
+
+ // Use "monolith" theme, as a nice balance between "nano" (does
+ // not work in Internet Explorer) and "classic" (too big)
+ theme : 'monolith',
+
+ // Leverage the container element as the button which shows the
+ // picker, relying on our own styling for that button
+ useAsButton : true,
+ appendToBody : true,
+
+ // Do not include opacity controls
+ lockOpacity : true,
+
+ // Include a selection of palette entries for convenience and
+ // reference
+ swatches : [],
+
+ components: {
+
+ // Include hue and color preview controls
+ preview : true,
+ hue : true,
+
+ // Display only a text color input field and the save and
+ // cancel buttons (no clear button)
+ interaction: {
+ input : true,
+ save : true,
+ cancel : true
+ }
+
+ }
+
+ });
+
+ // Hide color picker after user clicks "cancel"
+ pickr.on('cancel', function colorChangeCanceled() {
+ completeActiveRequest();
+ });
+
+ // Keep model in sync with changes to the color picker
+ pickr.on('save', function colorChanged(color) {
+ completeActiveRequest(color.toHEXA().toString());
+ activeRequest = null;
+ });
+
+ // Keep color picker in sync with changes to the model
+ pickr.on('init', function pickrReady() {
+ pickrInitComplete = true;
+ });
+ }
+ catch (e) {
+ // If the "Pickr" color picker cannot be loaded (Internet Explorer),
+ // the available flag will remain set to false
+ }
+
+ // Factory method required by provider
+ this.$get = ['$injector', function colorPickerServiceFactory($injector) {
+
+ // Required services
+ var $q = $injector.get('$q');
+ var $translate = $injector.get('$translate');
+
+ var service = {};
+
+ /**
+ * Promise which is resolved when Pickr initialization has completed
+ * and rejected if Pickr cannot be used.
+ *
+ * @type {Promise}
+ */
+ var pickrPromise = (function getPickr() {
+
+ var deferred = $q.defer();
+
+ // Resolve promise when Pickr has completed initialization
+ if (pickrInitComplete)
+ deferred.resolve();
+ else if (pickr)
+ pickr.on('init', deferred.resolve);
+
+ // Reject promise if Pickr cannot be used at all
+ else
+ deferred.reject();
+
+ return deferred.promise;
+
+ })();
+
+ /**
+ * Returns whether the underlying color picker (Pickr) can be used by
+ * calling selectColor(). If the browser cannot support the color
+ * picker, false is returned.
+ *
+ * @returns {Boolean}
+ * true if the underlying color picker can be used by calling
+ * selectColor(), false otherwise.
+ */
+ service.isAvailable = function isAvailable() {
+ return !!pickr;
+ };
+
+ /**
+ * Prompts the user to choose a color, returning the color chosen via a
+ * Promise.
+ *
+ * @param {Element} element
+ * The element that the user interacted with to indicate their
+ * desire to choose a color.
+ *
+ * @param {String} current
+ * The color that should be selected by default, in standard
+ * 6-digit hexadecimal RGB format, including "#" prefix.
+ *
+ * @param {String[]} [palette]
+ * An array of color choices which should be exposed to the user
+ * within the color chooser for convenience. Each color must be in
+ * standard 6-digit hexadecimal RGB format, including "#" prefix.
+ *
+ * @returns {Promise.<String>}
+ * A Promise which is resolved with the color chosen by the user,
+ * in standard 6-digit hexadecimal RGB format with "#" prefix, and
+ * rejected if the selection operation was cancelled or the color
+ * picker cannot be used.
+ */
+ service.selectColor = function selectColor(element, current, palette) {
+
+ // Show picker once the relevant translation strings have been
+ // retrieved and Pickr is ready for use
+ return $q.all({
+ 'saveString' : $translate('APP.ACTION_SAVE'),
+ 'cancelString' : $translate('APP.ACTION_CANCEL'),
+ 'pickr' : pickrPromise
+ }).then(function dependenciesReady(deps) {
+
+ // Cancel any active request
+ completeActiveRequest();
+
+ // Reset state of color picker to provided parameters
+ pickr.setColor(current);
+ element.appendChild(pickerContainer);
+
+ // Assign translated strings to button text
+ var pickrRoot = pickr.getRoot();
+ pickrRoot.interaction.save.value = deps.saveString;
+ pickrRoot.interaction.cancel.value = deps.cancelString;
+
+ // Replace all color swatches with the palette of colors given
+ while (pickr.removeSwatch(0)) {}
+ angular.forEach(palette, pickr.addSwatch.bind(pickr));
+
+ // Show color picker and wait for user to complete selection
+ activeRequest = $q.defer();
+ pickr.show();
+ return activeRequest.promise;
+
+ });
+
+ };
+
+ return service;
+
+ }];
+
+});
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/form/services/formService.js b/guacamole/src/main/webapp/app/form/services/formService.js
index 7e9c1cb..1df49dc 100644
--- a/guacamole/src/main/webapp/app/form/services/formService.js
+++ b/guacamole/src/main/webapp/app/form/services/formService.js
@@ -179,6 +179,19 @@
module : 'form',
controller : 'timeFieldController',
templateUrl : 'app/form/templates/timeField.html'
+ },
+
+ /**
+ * Field type which allows selection of color schemes accepted by the
+ * Guacamole server terminal emulator and protocols which leverage it.
+ *
+ * @see {@link Field.Type.TERMINAL_COLOR_SCHEME}
+ * @type FieldType
+ */
+ 'TERMINAL_COLOR_SCHEME' : {
+ module : 'form',
+ controller : 'terminalColorSchemeFieldController',
+ templateUrl : 'app/form/templates/terminalColorSchemeField.html'
}
};
@@ -207,11 +220,46 @@
var $q = $injector.get('$q');
var $templateRequest = $injector.get('$templateRequest');
+ /**
+ * Map of module name to the injector instance created for that module.
+ *
+ * @type {Object.<String, injector>}
+ */
+ var injectors = {};
+
var service = {};
service.fieldTypes = provider.fieldTypes;
/**
+ * Given the name of a module, returns an injector instance which
+ * injects dependencies within that module. A new injector may be
+ * created and initialized if no such injector has yet been requested.
+ * If the injector available to formService already includes the
+ * requested module, that injector will simply be returned.
+ *
+ * @param {String} module
+ * The name of the module to produce an injector for.
+ *
+ * @returns {injector}
+ * An injector instance which injects dependencies for the given
+ * module.
+ */
+ var getInjector = function getInjector(module) {
+
+ // Use the formService's injector if possible
+ if ($injector.modules[module])
+ return $injector;
+
+ // If the formService's injector does not include the requested
+ // module, create the necessary injector, reusing that injector for
+ // future calls
+ injectors[module] = injectors[module] || angular.injector(['ng', module]);
+ return injectors[module];
+
+ };
+
+ /**
* Compiles and links the field associated with the given name to the given
* scope, producing a distinct and independent DOM Element which functions
* as an instance of that field. The scope object provided must include at
@@ -221,6 +269,11 @@
* A String which defines the unique namespace associated the
* translation strings used by the form using a field of this type.
*
+ * fieldId:
+ * A String value which is reasonably likely to be unique and may
+ * be used to associate the main element of the field with its
+ * label.
+ *
* field:
* The Field object that is being rendered, representing a field of
* this type.
@@ -286,7 +339,7 @@
// Populate scope using defined controller
if (fieldType.module && fieldType.controller) {
- var $controller = angular.injector(['ng', fieldType.module]).get('$controller');
+ var $controller = getInjector(fieldType.module).get('$controller');
$controller(fieldType.controller, {
'$scope' : scope,
'$element' : angular.element(fieldContainer.childNodes)
diff --git a/guacamole/src/main/webapp/app/form/styles/terminal-color-scheme-field.css b/guacamole/src/main/webapp/app/form/styles/terminal-color-scheme-field.css
new file mode 100644
index 0000000..01eac1a
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/styles/terminal-color-scheme-field.css
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ */
+
+.terminal-color-scheme-field {
+ max-width: 320px;
+}
+
+.terminal-color-scheme-field select {
+ width: 100%;
+}
+
+.terminal-color-scheme-field .custom-color-scheme {
+ background: #EEE;
+ padding: 0.5em;
+ border: 1px solid silver;
+ border-spacing: 0;
+ margin-top: -2px;
+ width: 100%;
+}
+
+.terminal-color-scheme-field .custom-color-scheme-section {
+ display: -ms-flexbox;
+ display: -moz-box;
+ display: -webkit-box;
+ display: -webkit-flex;
+ display: flex;
+}
+
+.terminal-color-scheme-field .guac-input-color {
+
+ display: block;
+ margin: 2px;
+ width: 1.5em;
+ height: 1.5em;
+ min-width: 1.25em;
+ border-radius: 0.15em;
+ line-height: 1.5em;
+ text-align: center;
+ font-size: 0.75em;
+ cursor: pointer;
+ color: black;
+
+ -ms-flex: 1;
+ -moz-box-flex: 1;
+ -webkit-box-flex: 1;
+ -webkit-flex: 1;
+ flex: 1;
+
+}
+
+.terminal-color-scheme-field .guac-input-color.read-only {
+ cursor: not-allowed;
+}
+
+.terminal-color-scheme-field .guac-input-color.dark {
+ color: white;
+}
+
+.terminal-color-scheme-field .palette .guac-input-color {
+ font-weight: bold;
+}
+
+/* Hide palette numbers unless color scheme details are visible */
+.terminal-color-scheme-field.custom-color-scheme-details-hidden .custom-color-scheme .palette .guac-input-color {
+ color: transparent;
+}
+
+/*
+ * Custom color scheme details header
+ */
+
+.terminal-color-scheme-field .custom-color-scheme-details-header {
+ font-size: 0.8em;
+ margin: 0.5em 0;
+ padding: 0;
+}
+
+.terminal-color-scheme-field .custom-color-scheme-details-header::before {
+ content: 'â–¸ ';
+}
+
+.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-details-header::before {
+ content: 'â–¾ ';
+}
+
+/*
+ * Details show/hide link
+ */
+
+/* Render show/hide as a link */
+.terminal-color-scheme-field .custom-color-scheme-hide-details,
+.terminal-color-scheme-field .custom-color-scheme-show-details {
+ color: blue;
+ text-decoration: underline;
+ cursor: pointer;
+ margin: 0 0.25em;
+ font-weight: normal;
+}
+
+.terminal-color-scheme-field .custom-color-scheme-hide-details {
+ display: none;
+}
+
+.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-hide-details {
+ display: inline;
+}
+
+.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-show-details {
+ display: none;
+}
+
+/*
+ * Color scheme details
+ */
+
+.terminal-color-scheme-field .custom-color-scheme-details {
+ display: none;
+}
+
+.terminal-color-scheme-field.custom-color-scheme-details-visible .custom-color-scheme-details {
+ display: block;
+ width: 100%;
+ margin: 0.5em 0;
+}
+
+/*
+ * Color picker
+ */
+
+/* Increase width of color picker to allow two even rows of eight color
+ * swatches */
+.guac-input-color-picker[data-theme="monolith"] {
+ width: 16.25em;
+}
+
+/* Remove Guacamole-specific styles inherited from the generic button rules */
+.guac-input-color-picker[data-theme="monolith"] button {
+ min-width: 0;
+ padding: 0;
+ margin: 0;
+ box-shadow: none;
+}
diff --git a/guacamole/src/main/webapp/app/form/templates/checkboxField.html b/guacamole/src/main/webapp/app/form/templates/checkboxField.html
index 252441b..e906f7d 100644
--- a/guacamole/src/main/webapp/app/form/templates/checkboxField.html
+++ b/guacamole/src/main/webapp/app/form/templates/checkboxField.html
@@ -1 +1,7 @@
-<input type="checkbox" ng-disabled="disabled" ng-model="typedValue" guac-focus="focused" autocorrect="off" autocapitalize="off"/>
+<input type="checkbox"
+ ng-attr-id="{{ fieldId }}"
+ ng-disabled="disabled"
+ ng-model="typedValue"
+ guac-focus="focused"
+ autocorrect="off"
+ autocapitalize="off"/>
diff --git a/guacamole/src/main/webapp/app/form/templates/dateField.html b/guacamole/src/main/webapp/app/form/templates/dateField.html
index 6fd38da..7673e36 100644
--- a/guacamole/src/main/webapp/app/form/templates/dateField.html
+++ b/guacamole/src/main/webapp/app/form/templates/dateField.html
@@ -1,6 +1,7 @@
<div class="date-field">
<input type="date"
ng-disabled="disabled"
+ ng-attr-id="{{ fieldId }}"
ng-model="typedValue"
ng-model-options="modelOptions"
guac-lenient-date
diff --git a/guacamole/src/main/webapp/app/form/templates/emailField.html b/guacamole/src/main/webapp/app/form/templates/emailField.html
index 3eb31e7..cbfbb90 100644
--- a/guacamole/src/main/webapp/app/form/templates/emailField.html
+++ b/guacamole/src/main/webapp/app/form/templates/emailField.html
@@ -1,10 +1,11 @@
<div class="email-field">
<input type="email"
ng-disabled="disabled"
+ ng-attr-id="{{ fieldId }}"
ng-model="model"
ng-hide="readOnly"
guac-focus="focused"
autocorrect="off"
autocapitalize="off"/>
<a href="mailto:{{model}}" ng-show="readOnly">{{model}}</a>
-</div>
\ No newline at end of file
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/formField.html b/guacamole/src/main/webapp/app/form/templates/formField.html
index 45cf6b9..3e45d4c 100644
--- a/guacamole/src/main/webapp/app/form/templates/formField.html
+++ b/guacamole/src/main/webapp/app/form/templates/formField.html
@@ -1,9 +1,11 @@
-<label class="labeled-field" ng-class="{empty: !model}" ng-show="isFieldVisible()">
+<div class="labeled-field" ng-class="{empty: !model}" ng-show="isFieldVisible()">
<!-- Field header -->
- <span class="field-header">{{getFieldHeader() | translate}}</span>
+ <div class="field-header">
+ <label ng-attr-for="{{ fieldId }}">{{getFieldHeader() | translate}}</label>
+ </div>
<!-- Field content -->
<div class="form-field"></div>
-</label>
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/guacInputColor.html b/guacamole/src/main/webapp/app/form/templates/guacInputColor.html
new file mode 100644
index 0000000..eae1f66
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/templates/guacInputColor.html
@@ -0,0 +1,11 @@
+<div class="guac-input-color"
+ ng-class="{
+ 'dark' : isDark(),
+ 'read-only' : !isColorPickerAvailable()
+ }"
+ ng-click="selectColor()"
+ ng-style="{
+ 'background-color' : model
+ }">
+ <ng-transclude></ng-transclude>
+</div>
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/form/templates/languageField.html b/guacamole/src/main/webapp/app/form/templates/languageField.html
index 5af4e75..2a22ff2 100644
--- a/guacamole/src/main/webapp/app/form/templates/languageField.html
+++ b/guacamole/src/main/webapp/app/form/templates/languageField.html
@@ -1 +1,4 @@
-<select guac-focus="focused" ng-model="model" ng-options="language.key as language.value for language in languages | toArray | orderBy: key"></select>
\ No newline at end of file
+<select guac-focus="focused"
+ ng-attr-id="{{ fieldId }}"
+ ng-model="model"
+ ng-options="language.key as language.value for language in languages | toArray | orderBy: key"></select>
diff --git a/guacamole/src/main/webapp/app/form/templates/numberField.html b/guacamole/src/main/webapp/app/form/templates/numberField.html
index 9e4c28b..c86fb8e 100644
--- a/guacamole/src/main/webapp/app/form/templates/numberField.html
+++ b/guacamole/src/main/webapp/app/form/templates/numberField.html
@@ -1 +1,7 @@
-<input type="number" ng-disabled="disabled" ng-model="typedValue" guac-focus="focused" autocorrect="off" autocapitalize="off"/>
+<input type="number"
+ ng-disabled="disabled"
+ ng-attr-id="{{ fieldId }}"
+ ng-model="typedValue"
+ guac-focus="focused"
+ autocorrect="off"
+ autocapitalize="off"/>
diff --git a/guacamole/src/main/webapp/app/form/templates/passwordField.html b/guacamole/src/main/webapp/app/form/templates/passwordField.html
index 68d36f7..35eba9e 100644
--- a/guacamole/src/main/webapp/app/form/templates/passwordField.html
+++ b/guacamole/src/main/webapp/app/form/templates/passwordField.html
@@ -1,4 +1,11 @@
<div class="password-field">
- <input type="{{passwordInputType}}" ng-disabled="disabled" ng-model="model" ng-trim="false" guac-focus="focused" autocorrect="off" autocapitalize="off"/>
+ <input type="{{passwordInputType}}"
+ ng-disabled="disabled"
+ ng-attr-id="{{ fieldId }}"
+ ng-model="model"
+ ng-trim="false"
+ guac-focus="focused"
+ autocorrect="off"
+ autocapitalize="off"/>
<div class="icon toggle-password" ng-click="togglePassword()" title="{{getTogglePasswordHelpText() | translate}}"></div>
-</div>
\ No newline at end of file
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/selectField.html b/guacamole/src/main/webapp/app/form/templates/selectField.html
index f173d54..2c672aa 100644
--- a/guacamole/src/main/webapp/app/form/templates/selectField.html
+++ b/guacamole/src/main/webapp/app/form/templates/selectField.html
@@ -1,2 +1,5 @@
-<select ng-model="model" ng-disabled="disabled" guac-focus="focused"
+<select ng-attr-id="{{ fieldId }}"
+ ng-disabled="disabled"
+ guac-focus="focused"
+ ng-model="model"
ng-options="option as getFieldOption(option) | translate for option in field.options | orderBy: value"></select>
diff --git a/guacamole/src/main/webapp/app/form/templates/terminalColorSchemeField.html b/guacamole/src/main/webapp/app/form/templates/terminalColorSchemeField.html
new file mode 100644
index 0000000..a8425e4
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/templates/terminalColorSchemeField.html
@@ -0,0 +1,63 @@
+<div class="terminal-color-scheme-field" ng-class="{
+ 'custom-color-scheme-details-visible' : detailsShown,
+ 'custom-color-scheme-details-hidden' : !detailsShown
+ }">
+
+ <!-- Pre-defined color scheme options -->
+ <select ng-attr-id="{{ fieldId }}" ng-model="selectedColorScheme">
+ <option ng-repeat="option in field.options | orderBy: value"
+ ng-value="option">{{ getFieldOption(option) | translate }}</option>
+ <option value="custom">{{ 'COLOR_SCHEME.FIELD_OPTION_CUSTOM' | translate }}</option>
+ </select>
+
+ <!-- Custom color scheme -->
+ <div class="custom-color-scheme" ng-show="isCustom()">
+
+ <!-- Default foreground color -->
+ <div class="custom-color-scheme-section default-color foreground">
+ <guac-input-color model="customColorScheme.foreground"
+ palette="defaultPalette">
+ {{ 'COLOR_SCHEME.FIELD_HEADER_FOREGROUND' | translate }}
+ </guac-input-color>
+ </div>
+
+ <!-- Default background color -->
+ <div class="custom-color-scheme-section default-color background">
+ <guac-input-color model="customColorScheme.background"
+ palette="defaultPalette">
+ {{ 'COLOR_SCHEME.FIELD_HEADER_BACKGROUND' | translate }}
+ </guac-input-color>
+ </div>
+
+ <!-- Low intensity portion of 16-color palette -->
+ <div class="custom-color-scheme-section palette low-intensity">
+ <guac-input-color ng-repeat="index in lowIntensity"
+ model="customColorScheme.colors[index]"
+ palette="defaultPalette">
+ {{ index }}
+ </guac-input-color>
+ </div>
+
+ <!-- High intensity portion of 16-color palette -->
+ <div class="custom-color-scheme-section palette high-intensity">
+ <guac-input-color ng-repeat="index in highIntensity"
+ model="customColorScheme.colors[index]"
+ palette="defaultPalette">
+ {{ index }}
+ </guac-input-color>
+ </div>
+
+ </div>
+
+ <!-- Show/hide details -->
+ <h4 class="custom-color-scheme-details-header" ng-show="isCustom()">
+ {{'COLOR_SCHEME.SECTION_HEADER_DETAILS' | translate}}
+ <a class="custom-color-scheme-show-details" ng-click="showDetails()">{{'COLOR_SCHEME.ACTION_SHOW_DETAILS' | translate}}</a>
+ <a class="custom-color-scheme-hide-details" ng-click="hideDetails()">{{'COLOR_SCHEME.ACTION_HIDE_DETAILS' | translate}}</a>
+ </h4>
+
+ <!-- Custom color scheme details (internal representation -->
+ <textarea class="custom-color-scheme-details" spellcheck="false"
+ ng-model="model" ng-show="isCustom()"></textarea>
+
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/textAreaField.html b/guacamole/src/main/webapp/app/form/templates/textAreaField.html
index 6614bfb..9761af7 100644
--- a/guacamole/src/main/webapp/app/form/templates/textAreaField.html
+++ b/guacamole/src/main/webapp/app/form/templates/textAreaField.html
@@ -1 +1,6 @@
-<textarea ng-model="model" guac-focus="focused" autocorrect="off" autocapitalize="off" ng-disabled="disabled"></textarea>
+<textarea ng-attr-id="{{ fieldId }}"
+ ng-model="model"
+ ng-disabled="disabled"
+ guac-focus="focused"
+ autocorrect="off"
+ autocapitalize="off"></textarea>
diff --git a/guacamole/src/main/webapp/app/form/templates/textField.html b/guacamole/src/main/webapp/app/form/templates/textField.html
index 6abd4f6..3aea2bc 100644
--- a/guacamole/src/main/webapp/app/form/templates/textField.html
+++ b/guacamole/src/main/webapp/app/form/templates/textField.html
@@ -1,8 +1,14 @@
<div class="text-field">
- <input type="text" ng-model="model" autocorrect="off" autocapitalize="off" guac-focus="focused"
- ng-disabled="disabled" ng-attr-list="{{ dataListId }}"/>
- <datalist ng-if="dataListId" id="{{ dataListId }}">
+ <input type="text"
+ ng-attr-id="{{ fieldId }}"
+ ng-attr-list="{{ dataListId }}"
+ ng-model="model"
+ ng-disabled="disabled"
+ guac-focus="focused"
+ autocorrect="off"
+ autocapitalize="off"/>
+ <datalist ng-if="dataListId" ng-attr-id="{{ dataListId }}">
<option ng-repeat="option in field.options | orderBy: option"
value="{{ option }}">{{ getFieldOption(option) | translate }}</option>
</datalist>
-</div>
\ No newline at end of file
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/timeField.html b/guacamole/src/main/webapp/app/form/templates/timeField.html
index 6f7201f..2a88230 100644
--- a/guacamole/src/main/webapp/app/form/templates/timeField.html
+++ b/guacamole/src/main/webapp/app/form/templates/timeField.html
@@ -1,6 +1,7 @@
<div class="time-field">
<input type="time"
ng-disabled="disabled"
+ ng-attr-id="{{ fieldId }}"
ng-model="typedValue"
ng-model-options="modelOptions"
guac-focus="focused"
diff --git a/guacamole/src/main/webapp/app/form/templates/timeZoneField.html b/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
index c318a17..ecab57d 100644
--- a/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
+++ b/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
@@ -4,6 +4,7 @@
<select class="time-zone-region"
ng-disabled="disabled"
guac-focus="focused"
+ ng-attr-id="{{ fieldId }}"
ng-model="region"
ng-options="name for name in regions | orderBy: name"></select>
diff --git a/guacamole/src/main/webapp/app/form/types/ColorScheme.js b/guacamole/src/main/webapp/app/form/types/ColorScheme.js
new file mode 100644
index 0000000..f51a667
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/types/ColorScheme.js
@@ -0,0 +1,949 @@
+/*
+ * 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.
+ */
+
+/**
+ * Service which defines the ColorScheme class.
+ */
+angular.module('form').factory('ColorScheme', [function defineColorScheme() {
+
+ /**
+ * Intermediate representation of a custom color scheme which can be
+ * converted to the color scheme format used by Guacamole's terminal
+ * emulator. All colors must be represented in the six-digit hexadecimal
+ * RGB notation used by HTML ("#000000" for black, etc.).
+ *
+ * @constructor
+ * @param {ColorScheme|Object} [template={}]
+ * The object whose properties should be copied within the new
+ * ColorScheme.
+ */
+ var ColorScheme = function ColorScheme(template) {
+
+ // Use empty object by default
+ template = template || {};
+
+ /**
+ * The terminal background color. This will be the default foreground
+ * color of the Guacamole terminal emulator ("#000000") by default.
+ *
+ * @type {String}
+ */
+ this.background = template.background || '#000000';
+
+ /**
+ * The terminal foreground color. This will be the default foreground
+ * color of the Guacamole terminal emulator ("#999999") by default.
+ *
+ * @type {String}
+ */
+ this.foreground = template.foreground || '#999999';
+
+ /**
+ * The terminal color palette. Default values are provided for the
+ * normal 16 terminal colors using the default values of the Guacamole
+ * terminal emulator, however the terminal emulator and this
+ * representation support up to 256 colors.
+ *
+ * @type {String[]}
+ */
+ this.colors = template.colors || [
+
+ // Normal colors
+ '#000000', // Black
+ '#993E3E', // Red
+ '#3E993E', // Green
+ '#99993E', // Brown
+ '#3E3E99', // Blue
+ '#993E99', // Magenta
+ '#3E9999', // Cyan
+ '#999999', // White
+
+ // Intense colors
+ '#3E3E3E', // Black
+ '#FF6767', // Red
+ '#67FF67', // Green
+ '#FFFF67', // Brown
+ '#6767FF', // Blue
+ '#FF67FF', // Magenta
+ '#67FFFF', // Cyan
+ '#FFFFFF' // White
+
+ ];
+
+ /**
+ * The string which was parsed to produce this ColorScheme instance, if
+ * ColorScheme.fromString() was used to produce this ColorScheme.
+ *
+ * @private
+ * @type {String}
+ */
+ this._originalString = template._originalString;
+
+ };
+
+ /**
+ * Given a color string in the standard 6-digit hexadecimal RGB format,
+ * returns a X11 color spec which represents the same color.
+ *
+ * @param {String} color
+ * The hexadecimal color string to convert.
+ *
+ * @returns {String}
+ * The X11 color spec representing the same color as the given
+ * hexadecimal string, or null if the given string is not a valid
+ * 6-digit hexadecimal RGB color.
+ */
+ var fromHexColor = function fromHexColor(color) {
+
+ var groups = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec(color);
+ if (!groups)
+ return null;
+
+ return 'rgb:' + groups[1] + '/' + groups[2] + '/' + groups[3];
+
+ };
+
+ /**
+ * Parses the same subset of the X11 color spec supported by the Guacamole
+ * terminal emulator (the "rgb:*" format), returning the equivalent 6-digit
+ * hexadecimal color string supported by the ColorScheme representation.
+ * The X11 color spec defined by Xlib's XParseColor(). The human-readable
+ * color names supported by the Guacamole terminal emulator (the same color
+ * names as supported by xterm) may also be used.
+ *
+ * @param {String} color
+ * The X11 color spec to parse, or the name of a known named color.
+ *
+ * @returns {String}
+ * The 6-digit hexadecimal color string which represents the same color
+ * as the given X11 color spec/name, or null if the given spec/name is
+ * invalid.
+ */
+ var toHexColor = function toHexColor(color) {
+
+ /**
+ * Shifts or truncates the given hexadecimal string such that it
+ * contains exactly two hexadecimal digits, as required by any
+ * individual color component of the 6-digit hexadecimal RGB format.
+ *
+ * @param {String} component
+ * The hexadecimal string to shift or truncate to two digits.
+ *
+ * @returns {String}
+ * A new 2-digit hexadecimal string containing the same digits as
+ * the provided string, shifted or truncated as necessary to fit
+ * within the 2-digit length limit.
+ */
+ var toHexComponent = function toHexComponent(component) {
+ return (component + '0').substring(0, 2).toUpperCase();
+ };
+
+ // Attempt to parse any non-RGB color as a named color
+ var groups = /^rgb:([0-9A-Fa-f]{1,4})\/([0-9A-Fa-f]{1,4})\/([0-9A-Fa-f]{1,4})$/.exec(color);
+ if (!groups)
+ return ColorScheme.NAMED_COLORS[color.toLowerCase()] || null;
+
+ // Convert to standard 6-digit hexadecimal RGB format
+ return '#' + toHexComponent(groups[1]) + toHexComponent(groups[2]) + toHexComponent(groups[3]);
+
+ };
+
+ /**
+ * Converts the given string representation of a color scheme which is
+ * supported by the Guacamole terminal emulator to a corresponding,
+ * intermediate ColorScheme object.
+ *
+ * @param {String} str
+ * An arbitrary color scheme, in the string format supported by the
+ * Guacamole terminal emulator.
+ *
+ * @returns {ColorScheme}
+ * A new ColorScheme instance which represents the same color scheme as
+ * the given string.
+ */
+ ColorScheme.fromString = function fromString(str) {
+
+ var scheme = new ColorScheme({ _originalString : str });
+
+ // For each semicolon-separated statement in the provided color scheme
+ var statements = str.split(/;/);
+ for (var i = 0; i < statements.length; i++) {
+
+ // Skip any statements which cannot be parsed
+ var statement = statements[i];
+ var groups = /^\s*(background|foreground|color([0-9]+))\s*:\s*(\S*)\s*$/.exec(statement);
+ if (!groups)
+ continue;
+
+ // If the statement is valid and contains a valid color, map that
+ // color to the appropriate property of the ColorScheme object
+ var color = toHexColor(groups[3]);
+ if (color) {
+ if (groups[1] === 'background')
+ scheme.background = color;
+ else if (groups[1] === 'foreground')
+ scheme.foreground = color;
+ else
+ scheme.colors[parseInt(groups[2])] = color;
+ }
+
+ }
+
+ return scheme;
+
+ };
+
+ /**
+ * Returns whether the two given color schemes define the exact same
+ * colors.
+ *
+ * @param {ColorScheme} a
+ * The first ColorScheme to compare.
+ *
+ * @param {ColorScheme} b
+ * The second ColorScheme to compare.
+ *
+ * @returns {Boolean}
+ * true if both color schemes contain the same colors, false otherwise.
+ */
+ ColorScheme.equals = function equals(a, b) {
+ return a.foreground === b.foreground
+ && a.background === b.background
+ && _.isEqual(a.colors, b.colors);
+ };
+
+ /**
+ * Converts the given ColorScheme to a string representation which is
+ * supported by the Guacamole terminal emulator.
+ *
+ * @param {ColorScheme} scheme
+ * The ColorScheme to convert to a string.
+ *
+ * @returns {String}
+ * The given color scheme, converted to the string format supported by
+ * the Guacamole terminal emulator.
+ */
+ ColorScheme.toString = function toString(scheme) {
+
+ // Use originally-provided string if it equates to the exact same color scheme
+ if (!_.isUndefined(scheme._originalString) && ColorScheme.equals(scheme, ColorScheme.fromString(scheme._originalString)))
+ return scheme._originalString;
+
+ // Add background and foreground
+ var str = 'background: ' + fromHexColor(scheme.background) + ';\n'
+ + 'foreground: ' + fromHexColor(scheme.foreground) + ';';
+
+ // Add color definitions for each palette entry
+ for (var index in scheme.colors)
+ str += '\ncolor' + index + ': ' + fromHexColor(scheme.colors[index]) + ';';
+
+ return str;
+
+ };
+
+ /**
+ * The set of all named colors supported by the Guacamole terminal
+ * emulator and their corresponding 6-digit hexadecimal RGB
+ * representations. This set should contain all colors supported by xterm.
+ *
+ * @constant
+ * @type {Object.<String, String>}
+ */
+ ColorScheme.NAMED_COLORS = {
+ 'aliceblue' : '#F0F8FF',
+ 'antiquewhite' : '#FAEBD7',
+ 'antiquewhite1' : '#FFEFDB',
+ 'antiquewhite2' : '#EEDFCC',
+ 'antiquewhite3' : '#CDC0B0',
+ 'antiquewhite4' : '#8B8378',
+ 'aqua' : '#00FFFF',
+ 'aquamarine' : '#7FFFD4',
+ 'aquamarine1' : '#7FFFD4',
+ 'aquamarine2' : '#76EEC6',
+ 'aquamarine3' : '#66CDAA',
+ 'aquamarine4' : '#458B74',
+ 'azure' : '#F0FFFF',
+ 'azure1' : '#F0FFFF',
+ 'azure2' : '#E0EEEE',
+ 'azure3' : '#C1CDCD',
+ 'azure4' : '#838B8B',
+ 'beige' : '#F5F5DC',
+ 'bisque' : '#FFE4C4',
+ 'bisque1' : '#FFE4C4',
+ 'bisque2' : '#EED5B7',
+ 'bisque3' : '#CDB79E',
+ 'bisque4' : '#8B7D6B',
+ 'black' : '#000000',
+ 'blanchedalmond' : '#FFEBCD',
+ 'blue' : '#0000FF',
+ 'blue1' : '#0000FF',
+ 'blue2' : '#0000EE',
+ 'blue3' : '#0000CD',
+ 'blue4' : '#00008B',
+ 'blueviolet' : '#8A2BE2',
+ 'brown' : '#A52A2A',
+ 'brown1' : '#FF4040',
+ 'brown2' : '#EE3B3B',
+ 'brown3' : '#CD3333',
+ 'brown4' : '#8B2323',
+ 'burlywood' : '#DEB887',
+ 'burlywood1' : '#FFD39B',
+ 'burlywood2' : '#EEC591',
+ 'burlywood3' : '#CDAA7D',
+ 'burlywood4' : '#8B7355',
+ 'cadetblue' : '#5F9EA0',
+ 'cadetblue1' : '#98F5FF',
+ 'cadetblue2' : '#8EE5EE',
+ 'cadetblue3' : '#7AC5CD',
+ 'cadetblue4' : '#53868B',
+ 'chartreuse' : '#7FFF00',
+ 'chartreuse1' : '#7FFF00',
+ 'chartreuse2' : '#76EE00',
+ 'chartreuse3' : '#66CD00',
+ 'chartreuse4' : '#458B00',
+ 'chocolate' : '#D2691E',
+ 'chocolate1' : '#FF7F24',
+ 'chocolate2' : '#EE7621',
+ 'chocolate3' : '#CD661D',
+ 'chocolate4' : '#8B4513',
+ 'coral' : '#FF7F50',
+ 'coral1' : '#FF7256',
+ 'coral2' : '#EE6A50',
+ 'coral3' : '#CD5B45',
+ 'coral4' : '#8B3E2F',
+ 'cornflowerblue' : '#6495ED',
+ 'cornsilk' : '#FFF8DC',
+ 'cornsilk1' : '#FFF8DC',
+ 'cornsilk2' : '#EEE8CD',
+ 'cornsilk3' : '#CDC8B1',
+ 'cornsilk4' : '#8B8878',
+ 'crimson' : '#DC143C',
+ 'cyan' : '#00FFFF',
+ 'cyan1' : '#00FFFF',
+ 'cyan2' : '#00EEEE',
+ 'cyan3' : '#00CDCD',
+ 'cyan4' : '#008B8B',
+ 'darkblue' : '#00008B',
+ 'darkcyan' : '#008B8B',
+ 'darkgoldenrod' : '#B8860B',
+ 'darkgoldenrod1' : '#FFB90F',
+ 'darkgoldenrod2' : '#EEAD0E',
+ 'darkgoldenrod3' : '#CD950C',
+ 'darkgoldenrod4' : '#8B6508',
+ 'darkgray' : '#A9A9A9',
+ 'darkgreen' : '#006400',
+ 'darkgrey' : '#A9A9A9',
+ 'darkkhaki' : '#BDB76B',
+ 'darkmagenta' : '#8B008B',
+ 'darkolivegreen' : '#556B2F',
+ 'darkolivegreen1' : '#CAFF70',
+ 'darkolivegreen2' : '#BCEE68',
+ 'darkolivegreen3' : '#A2CD5A',
+ 'darkolivegreen4' : '#6E8B3D',
+ 'darkorange' : '#FF8C00',
+ 'darkorange1' : '#FF7F00',
+ 'darkorange2' : '#EE7600',
+ 'darkorange3' : '#CD6600',
+ 'darkorange4' : '#8B4500',
+ 'darkorchid' : '#9932CC',
+ 'darkorchid1' : '#BF3EFF',
+ 'darkorchid2' : '#B23AEE',
+ 'darkorchid3' : '#9A32CD',
+ 'darkorchid4' : '#68228B',
+ 'darkred' : '#8B0000',
+ 'darksalmon' : '#E9967A',
+ 'darkseagreen' : '#8FBC8F',
+ 'darkseagreen1' : '#C1FFC1',
+ 'darkseagreen2' : '#B4EEB4',
+ 'darkseagreen3' : '#9BCD9B',
+ 'darkseagreen4' : '#698B69',
+ 'darkslateblue' : '#483D8B',
+ 'darkslategray' : '#2F4F4F',
+ 'darkslategray1' : '#97FFFF',
+ 'darkslategray2' : '#8DEEEE',
+ 'darkslategray3' : '#79CDCD',
+ 'darkslategray4' : '#528B8B',
+ 'darkslategrey' : '#2F4F4F',
+ 'darkturquoise' : '#00CED1',
+ 'darkviolet' : '#9400D3',
+ 'deeppink' : '#FF1493',
+ 'deeppink1' : '#FF1493',
+ 'deeppink2' : '#EE1289',
+ 'deeppink3' : '#CD1076',
+ 'deeppink4' : '#8B0A50',
+ 'deepskyblue' : '#00BFFF',
+ 'deepskyblue1' : '#00BFFF',
+ 'deepskyblue2' : '#00B2EE',
+ 'deepskyblue3' : '#009ACD',
+ 'deepskyblue4' : '#00688B',
+ 'dimgray' : '#696969',
+ 'dimgrey' : '#696969',
+ 'dodgerblue' : '#1E90FF',
+ 'dodgerblue1' : '#1E90FF',
+ 'dodgerblue2' : '#1C86EE',
+ 'dodgerblue3' : '#1874CD',
+ 'dodgerblue4' : '#104E8B',
+ 'firebrick' : '#B22222',
+ 'firebrick1' : '#FF3030',
+ 'firebrick2' : '#EE2C2C',
+ 'firebrick3' : '#CD2626',
+ 'firebrick4' : '#8B1A1A',
+ 'floralwhite' : '#FFFAF0',
+ 'forestgreen' : '#228B22',
+ 'fuchsia' : '#FF00FF',
+ 'gainsboro' : '#DCDCDC',
+ 'ghostwhite' : '#F8F8FF',
+ 'gold' : '#FFD700',
+ 'gold1' : '#FFD700',
+ 'gold2' : '#EEC900',
+ 'gold3' : '#CDAD00',
+ 'gold4' : '#8B7500',
+ 'goldenrod' : '#DAA520',
+ 'goldenrod1' : '#FFC125',
+ 'goldenrod2' : '#EEB422',
+ 'goldenrod3' : '#CD9B1D',
+ 'goldenrod4' : '#8B6914',
+ 'gray' : '#BEBEBE',
+ 'gray0' : '#000000',
+ 'gray1' : '#030303',
+ 'gray10' : '#1A1A1A',
+ 'gray100' : '#FFFFFF',
+ 'gray11' : '#1C1C1C',
+ 'gray12' : '#1F1F1F',
+ 'gray13' : '#212121',
+ 'gray14' : '#242424',
+ 'gray15' : '#262626',
+ 'gray16' : '#292929',
+ 'gray17' : '#2B2B2B',
+ 'gray18' : '#2E2E2E',
+ 'gray19' : '#303030',
+ 'gray2' : '#050505',
+ 'gray20' : '#333333',
+ 'gray21' : '#363636',
+ 'gray22' : '#383838',
+ 'gray23' : '#3B3B3B',
+ 'gray24' : '#3D3D3D',
+ 'gray25' : '#404040',
+ 'gray26' : '#424242',
+ 'gray27' : '#454545',
+ 'gray28' : '#474747',
+ 'gray29' : '#4A4A4A',
+ 'gray3' : '#080808',
+ 'gray30' : '#4D4D4D',
+ 'gray31' : '#4F4F4F',
+ 'gray32' : '#525252',
+ 'gray33' : '#545454',
+ 'gray34' : '#575757',
+ 'gray35' : '#595959',
+ 'gray36' : '#5C5C5C',
+ 'gray37' : '#5E5E5E',
+ 'gray38' : '#616161',
+ 'gray39' : '#636363',
+ 'gray4' : '#0A0A0A',
+ 'gray40' : '#666666',
+ 'gray41' : '#696969',
+ 'gray42' : '#6B6B6B',
+ 'gray43' : '#6E6E6E',
+ 'gray44' : '#707070',
+ 'gray45' : '#737373',
+ 'gray46' : '#757575',
+ 'gray47' : '#787878',
+ 'gray48' : '#7A7A7A',
+ 'gray49' : '#7D7D7D',
+ 'gray5' : '#0D0D0D',
+ 'gray50' : '#7F7F7F',
+ 'gray51' : '#828282',
+ 'gray52' : '#858585',
+ 'gray53' : '#878787',
+ 'gray54' : '#8A8A8A',
+ 'gray55' : '#8C8C8C',
+ 'gray56' : '#8F8F8F',
+ 'gray57' : '#919191',
+ 'gray58' : '#949494',
+ 'gray59' : '#969696',
+ 'gray6' : '#0F0F0F',
+ 'gray60' : '#999999',
+ 'gray61' : '#9C9C9C',
+ 'gray62' : '#9E9E9E',
+ 'gray63' : '#A1A1A1',
+ 'gray64' : '#A3A3A3',
+ 'gray65' : '#A6A6A6',
+ 'gray66' : '#A8A8A8',
+ 'gray67' : '#ABABAB',
+ 'gray68' : '#ADADAD',
+ 'gray69' : '#B0B0B0',
+ 'gray7' : '#121212',
+ 'gray70' : '#B3B3B3',
+ 'gray71' : '#B5B5B5',
+ 'gray72' : '#B8B8B8',
+ 'gray73' : '#BABABA',
+ 'gray74' : '#BDBDBD',
+ 'gray75' : '#BFBFBF',
+ 'gray76' : '#C2C2C2',
+ 'gray77' : '#C4C4C4',
+ 'gray78' : '#C7C7C7',
+ 'gray79' : '#C9C9C9',
+ 'gray8' : '#141414',
+ 'gray80' : '#CCCCCC',
+ 'gray81' : '#CFCFCF',
+ 'gray82' : '#D1D1D1',
+ 'gray83' : '#D4D4D4',
+ 'gray84' : '#D6D6D6',
+ 'gray85' : '#D9D9D9',
+ 'gray86' : '#DBDBDB',
+ 'gray87' : '#DEDEDE',
+ 'gray88' : '#E0E0E0',
+ 'gray89' : '#E3E3E3',
+ 'gray9' : '#171717',
+ 'gray90' : '#E5E5E5',
+ 'gray91' : '#E8E8E8',
+ 'gray92' : '#EBEBEB',
+ 'gray93' : '#EDEDED',
+ 'gray94' : '#F0F0F0',
+ 'gray95' : '#F2F2F2',
+ 'gray96' : '#F5F5F5',
+ 'gray97' : '#F7F7F7',
+ 'gray98' : '#FAFAFA',
+ 'gray99' : '#FCFCFC',
+ 'green' : '#00FF00',
+ 'green1' : '#00FF00',
+ 'green2' : '#00EE00',
+ 'green3' : '#00CD00',
+ 'green4' : '#008B00',
+ 'greenyellow' : '#ADFF2F',
+ 'grey' : '#BEBEBE',
+ 'grey0' : '#000000',
+ 'grey1' : '#030303',
+ 'grey10' : '#1A1A1A',
+ 'grey100' : '#FFFFFF',
+ 'grey11' : '#1C1C1C',
+ 'grey12' : '#1F1F1F',
+ 'grey13' : '#212121',
+ 'grey14' : '#242424',
+ 'grey15' : '#262626',
+ 'grey16' : '#292929',
+ 'grey17' : '#2B2B2B',
+ 'grey18' : '#2E2E2E',
+ 'grey19' : '#303030',
+ 'grey2' : '#050505',
+ 'grey20' : '#333333',
+ 'grey21' : '#363636',
+ 'grey22' : '#383838',
+ 'grey23' : '#3B3B3B',
+ 'grey24' : '#3D3D3D',
+ 'grey25' : '#404040',
+ 'grey26' : '#424242',
+ 'grey27' : '#454545',
+ 'grey28' : '#474747',
+ 'grey29' : '#4A4A4A',
+ 'grey3' : '#080808',
+ 'grey30' : '#4D4D4D',
+ 'grey31' : '#4F4F4F',
+ 'grey32' : '#525252',
+ 'grey33' : '#545454',
+ 'grey34' : '#575757',
+ 'grey35' : '#595959',
+ 'grey36' : '#5C5C5C',
+ 'grey37' : '#5E5E5E',
+ 'grey38' : '#616161',
+ 'grey39' : '#636363',
+ 'grey4' : '#0A0A0A',
+ 'grey40' : '#666666',
+ 'grey41' : '#696969',
+ 'grey42' : '#6B6B6B',
+ 'grey43' : '#6E6E6E',
+ 'grey44' : '#707070',
+ 'grey45' : '#737373',
+ 'grey46' : '#757575',
+ 'grey47' : '#787878',
+ 'grey48' : '#7A7A7A',
+ 'grey49' : '#7D7D7D',
+ 'grey5' : '#0D0D0D',
+ 'grey50' : '#7F7F7F',
+ 'grey51' : '#828282',
+ 'grey52' : '#858585',
+ 'grey53' : '#878787',
+ 'grey54' : '#8A8A8A',
+ 'grey55' : '#8C8C8C',
+ 'grey56' : '#8F8F8F',
+ 'grey57' : '#919191',
+ 'grey58' : '#949494',
+ 'grey59' : '#969696',
+ 'grey6' : '#0F0F0F',
+ 'grey60' : '#999999',
+ 'grey61' : '#9C9C9C',
+ 'grey62' : '#9E9E9E',
+ 'grey63' : '#A1A1A1',
+ 'grey64' : '#A3A3A3',
+ 'grey65' : '#A6A6A6',
+ 'grey66' : '#A8A8A8',
+ 'grey67' : '#ABABAB',
+ 'grey68' : '#ADADAD',
+ 'grey69' : '#B0B0B0',
+ 'grey7' : '#121212',
+ 'grey70' : '#B3B3B3',
+ 'grey71' : '#B5B5B5',
+ 'grey72' : '#B8B8B8',
+ 'grey73' : '#BABABA',
+ 'grey74' : '#BDBDBD',
+ 'grey75' : '#BFBFBF',
+ 'grey76' : '#C2C2C2',
+ 'grey77' : '#C4C4C4',
+ 'grey78' : '#C7C7C7',
+ 'grey79' : '#C9C9C9',
+ 'grey8' : '#141414',
+ 'grey80' : '#CCCCCC',
+ 'grey81' : '#CFCFCF',
+ 'grey82' : '#D1D1D1',
+ 'grey83' : '#D4D4D4',
+ 'grey84' : '#D6D6D6',
+ 'grey85' : '#D9D9D9',
+ 'grey86' : '#DBDBDB',
+ 'grey87' : '#DEDEDE',
+ 'grey88' : '#E0E0E0',
+ 'grey89' : '#E3E3E3',
+ 'grey9' : '#171717',
+ 'grey90' : '#E5E5E5',
+ 'grey91' : '#E8E8E8',
+ 'grey92' : '#EBEBEB',
+ 'grey93' : '#EDEDED',
+ 'grey94' : '#F0F0F0',
+ 'grey95' : '#F2F2F2',
+ 'grey96' : '#F5F5F5',
+ 'grey97' : '#F7F7F7',
+ 'grey98' : '#FAFAFA',
+ 'grey99' : '#FCFCFC',
+ 'honeydew' : '#F0FFF0',
+ 'honeydew1' : '#F0FFF0',
+ 'honeydew2' : '#E0EEE0',
+ 'honeydew3' : '#C1CDC1',
+ 'honeydew4' : '#838B83',
+ 'hotpink' : '#FF69B4',
+ 'hotpink1' : '#FF6EB4',
+ 'hotpink2' : '#EE6AA7',
+ 'hotpink3' : '#CD6090',
+ 'hotpink4' : '#8B3A62',
+ 'indianred' : '#CD5C5C',
+ 'indianred1' : '#FF6A6A',
+ 'indianred2' : '#EE6363',
+ 'indianred3' : '#CD5555',
+ 'indianred4' : '#8B3A3A',
+ 'indigo' : '#4B0082',
+ 'ivory' : '#FFFFF0',
+ 'ivory1' : '#FFFFF0',
+ 'ivory2' : '#EEEEE0',
+ 'ivory3' : '#CDCDC1',
+ 'ivory4' : '#8B8B83',
+ 'khaki' : '#F0E68C',
+ 'khaki1' : '#FFF68F',
+ 'khaki2' : '#EEE685',
+ 'khaki3' : '#CDC673',
+ 'khaki4' : '#8B864E',
+ 'lavender' : '#E6E6FA',
+ 'lavenderblush' : '#FFF0F5',
+ 'lavenderblush1' : '#FFF0F5',
+ 'lavenderblush2' : '#EEE0E5',
+ 'lavenderblush3' : '#CDC1C5',
+ 'lavenderblush4' : '#8B8386',
+ 'lawngreen' : '#7CFC00',
+ 'lemonchiffon' : '#FFFACD',
+ 'lemonchiffon1' : '#FFFACD',
+ 'lemonchiffon2' : '#EEE9BF',
+ 'lemonchiffon3' : '#CDC9A5',
+ 'lemonchiffon4' : '#8B8970',
+ 'lightblue' : '#ADD8E6',
+ 'lightblue1' : '#BFEFFF',
+ 'lightblue2' : '#B2DFEE',
+ 'lightblue3' : '#9AC0CD',
+ 'lightblue4' : '#68838B',
+ 'lightcoral' : '#F08080',
+ 'lightcyan' : '#E0FFFF',
+ 'lightcyan1' : '#E0FFFF',
+ 'lightcyan2' : '#D1EEEE',
+ 'lightcyan3' : '#B4CDCD',
+ 'lightcyan4' : '#7A8B8B',
+ 'lightgoldenrod' : '#EEDD82',
+ 'lightgoldenrod1' : '#FFEC8B',
+ 'lightgoldenrod2' : '#EEDC82',
+ 'lightgoldenrod3' : '#CDBE70',
+ 'lightgoldenrod4' : '#8B814C',
+ 'lightgoldenrodyellow' : '#FAFAD2',
+ 'lightgray' : '#D3D3D3',
+ 'lightgreen' : '#90EE90',
+ 'lightgrey' : '#D3D3D3',
+ 'lightpink' : '#FFB6C1',
+ 'lightpink1' : '#FFAEB9',
+ 'lightpink2' : '#EEA2AD',
+ 'lightpink3' : '#CD8C95',
+ 'lightpink4' : '#8B5F65',
+ 'lightsalmon' : '#FFA07A',
+ 'lightsalmon1' : '#FFA07A',
+ 'lightsalmon2' : '#EE9572',
+ 'lightsalmon3' : '#CD8162',
+ 'lightsalmon4' : '#8B5742',
+ 'lightseagreen' : '#20B2AA',
+ 'lightskyblue' : '#87CEFA',
+ 'lightskyblue1' : '#B0E2FF',
+ 'lightskyblue2' : '#A4D3EE',
+ 'lightskyblue3' : '#8DB6CD',
+ 'lightskyblue4' : '#607B8B',
+ 'lightslateblue' : '#8470FF',
+ 'lightslategray' : '#778899',
+ 'lightslategrey' : '#778899',
+ 'lightsteelblue' : '#B0C4DE',
+ 'lightsteelblue1' : '#CAE1FF',
+ 'lightsteelblue2' : '#BCD2EE',
+ 'lightsteelblue3' : '#A2B5CD',
+ 'lightsteelblue4' : '#6E7B8B',
+ 'lightyellow' : '#FFFFE0',
+ 'lightyellow1' : '#FFFFE0',
+ 'lightyellow2' : '#EEEED1',
+ 'lightyellow3' : '#CDCDB4',
+ 'lightyellow4' : '#8B8B7A',
+ 'lime' : '#00FF00',
+ 'limegreen' : '#32CD32',
+ 'linen' : '#FAF0E6',
+ 'magenta' : '#FF00FF',
+ 'magenta1' : '#FF00FF',
+ 'magenta2' : '#EE00EE',
+ 'magenta3' : '#CD00CD',
+ 'magenta4' : '#8B008B',
+ 'maroon' : '#B03060',
+ 'maroon1' : '#FF34B3',
+ 'maroon2' : '#EE30A7',
+ 'maroon3' : '#CD2990',
+ 'maroon4' : '#8B1C62',
+ 'mediumaquamarine' : '#66CDAA',
+ 'mediumblue' : '#0000CD',
+ 'mediumorchid' : '#BA55D3',
+ 'mediumorchid1' : '#E066FF',
+ 'mediumorchid2' : '#D15FEE',
+ 'mediumorchid3' : '#B452CD',
+ 'mediumorchid4' : '#7A378B',
+ 'mediumpurple' : '#9370DB',
+ 'mediumpurple1' : '#AB82FF',
+ 'mediumpurple2' : '#9F79EE',
+ 'mediumpurple3' : '#8968CD',
+ 'mediumpurple4' : '#5D478B',
+ 'mediumseagreen' : '#3CB371',
+ 'mediumslateblue' : '#7B68EE',
+ 'mediumspringgreen' : '#00FA9A',
+ 'mediumturquoise' : '#48D1CC',
+ 'mediumvioletred' : '#C71585',
+ 'midnightblue' : '#191970',
+ 'mintcream' : '#F5FFFA',
+ 'mistyrose' : '#FFE4E1',
+ 'mistyrose1' : '#FFE4E1',
+ 'mistyrose2' : '#EED5D2',
+ 'mistyrose3' : '#CDB7B5',
+ 'mistyrose4' : '#8B7D7B',
+ 'moccasin' : '#FFE4B5',
+ 'navajowhite' : '#FFDEAD',
+ 'navajowhite1' : '#FFDEAD',
+ 'navajowhite2' : '#EECFA1',
+ 'navajowhite3' : '#CDB38B',
+ 'navajowhite4' : '#8B795E',
+ 'navy' : '#000080',
+ 'navyblue' : '#000080',
+ 'oldlace' : '#FDF5E6',
+ 'olive' : '#808000',
+ 'olivedrab' : '#6B8E23',
+ 'olivedrab1' : '#C0FF3E',
+ 'olivedrab2' : '#B3EE3A',
+ 'olivedrab3' : '#9ACD32',
+ 'olivedrab4' : '#698B22',
+ 'orange' : '#FFA500',
+ 'orange1' : '#FFA500',
+ 'orange2' : '#EE9A00',
+ 'orange3' : '#CD8500',
+ 'orange4' : '#8B5A00',
+ 'orangered' : '#FF4500',
+ 'orangered1' : '#FF4500',
+ 'orangered2' : '#EE4000',
+ 'orangered3' : '#CD3700',
+ 'orangered4' : '#8B2500',
+ 'orchid' : '#DA70D6',
+ 'orchid1' : '#FF83FA',
+ 'orchid2' : '#EE7AE9',
+ 'orchid3' : '#CD69C9',
+ 'orchid4' : '#8B4789',
+ 'palegoldenrod' : '#EEE8AA',
+ 'palegreen' : '#98FB98',
+ 'palegreen1' : '#9AFF9A',
+ 'palegreen2' : '#90EE90',
+ 'palegreen3' : '#7CCD7C',
+ 'palegreen4' : '#548B54',
+ 'paleturquoise' : '#AFEEEE',
+ 'paleturquoise1' : '#BBFFFF',
+ 'paleturquoise2' : '#AEEEEE',
+ 'paleturquoise3' : '#96CDCD',
+ 'paleturquoise4' : '#668B8B',
+ 'palevioletred' : '#DB7093',
+ 'palevioletred1' : '#FF82AB',
+ 'palevioletred2' : '#EE799F',
+ 'palevioletred3' : '#CD6889',
+ 'palevioletred4' : '#8B475D',
+ 'papayawhip' : '#FFEFD5',
+ 'peachpuff' : '#FFDAB9',
+ 'peachpuff1' : '#FFDAB9',
+ 'peachpuff2' : '#EECBAD',
+ 'peachpuff3' : '#CDAF95',
+ 'peachpuff4' : '#8B7765',
+ 'peru' : '#CD853F',
+ 'pink' : '#FFC0CB',
+ 'pink1' : '#FFB5C5',
+ 'pink2' : '#EEA9B8',
+ 'pink3' : '#CD919E',
+ 'pink4' : '#8B636C',
+ 'plum' : '#DDA0DD',
+ 'plum1' : '#FFBBFF',
+ 'plum2' : '#EEAEEE',
+ 'plum3' : '#CD96CD',
+ 'plum4' : '#8B668B',
+ 'powderblue' : '#B0E0E6',
+ 'purple' : '#A020F0',
+ 'purple1' : '#9B30FF',
+ 'purple2' : '#912CEE',
+ 'purple3' : '#7D26CD',
+ 'purple4' : '#551A8B',
+ 'rebeccapurple' : '#663399',
+ 'red' : '#FF0000',
+ 'red1' : '#FF0000',
+ 'red2' : '#EE0000',
+ 'red3' : '#CD0000',
+ 'red4' : '#8B0000',
+ 'rosybrown' : '#BC8F8F',
+ 'rosybrown1' : '#FFC1C1',
+ 'rosybrown2' : '#EEB4B4',
+ 'rosybrown3' : '#CD9B9B',
+ 'rosybrown4' : '#8B6969',
+ 'royalblue' : '#4169E1',
+ 'royalblue1' : '#4876FF',
+ 'royalblue2' : '#436EEE',
+ 'royalblue3' : '#3A5FCD',
+ 'royalblue4' : '#27408B',
+ 'saddlebrown' : '#8B4513',
+ 'salmon' : '#FA8072',
+ 'salmon1' : '#FF8C69',
+ 'salmon2' : '#EE8262',
+ 'salmon3' : '#CD7054',
+ 'salmon4' : '#8B4C39',
+ 'sandybrown' : '#F4A460',
+ 'seagreen' : '#2E8B57',
+ 'seagreen1' : '#54FF9F',
+ 'seagreen2' : '#4EEE94',
+ 'seagreen3' : '#43CD80',
+ 'seagreen4' : '#2E8B57',
+ 'seashell' : '#FFF5EE',
+ 'seashell1' : '#FFF5EE',
+ 'seashell2' : '#EEE5DE',
+ 'seashell3' : '#CDC5BF',
+ 'seashell4' : '#8B8682',
+ 'sienna' : '#A0522D',
+ 'sienna1' : '#FF8247',
+ 'sienna2' : '#EE7942',
+ 'sienna3' : '#CD6839',
+ 'sienna4' : '#8B4726',
+ 'silver' : '#C0C0C0',
+ 'skyblue' : '#87CEEB',
+ 'skyblue1' : '#87CEFF',
+ 'skyblue2' : '#7EC0EE',
+ 'skyblue3' : '#6CA6CD',
+ 'skyblue4' : '#4A708B',
+ 'slateblue' : '#6A5ACD',
+ 'slateblue1' : '#836FFF',
+ 'slateblue2' : '#7A67EE',
+ 'slateblue3' : '#6959CD',
+ 'slateblue4' : '#473C8B',
+ 'slategray' : '#708090',
+ 'slategray1' : '#C6E2FF',
+ 'slategray2' : '#B9D3EE',
+ 'slategray3' : '#9FB6CD',
+ 'slategray4' : '#6C7B8B',
+ 'slategrey' : '#708090',
+ 'snow' : '#FFFAFA',
+ 'snow1' : '#FFFAFA',
+ 'snow2' : '#EEE9E9',
+ 'snow3' : '#CDC9C9',
+ 'snow4' : '#8B8989',
+ 'springgreen' : '#00FF7F',
+ 'springgreen1' : '#00FF7F',
+ 'springgreen2' : '#00EE76',
+ 'springgreen3' : '#00CD66',
+ 'springgreen4' : '#008B45',
+ 'steelblue' : '#4682B4',
+ 'steelblue1' : '#63B8FF',
+ 'steelblue2' : '#5CACEE',
+ 'steelblue3' : '#4F94CD',
+ 'steelblue4' : '#36648B',
+ 'tan' : '#D2B48C',
+ 'tan1' : '#FFA54F',
+ 'tan2' : '#EE9A49',
+ 'tan3' : '#CD853F',
+ 'tan4' : '#8B5A2B',
+ 'teal' : '#008080',
+ 'thistle' : '#D8BFD8',
+ 'thistle1' : '#FFE1FF',
+ 'thistle2' : '#EED2EE',
+ 'thistle3' : '#CDB5CD',
+ 'thistle4' : '#8B7B8B',
+ 'tomato' : '#FF6347',
+ 'tomato1' : '#FF6347',
+ 'tomato2' : '#EE5C42',
+ 'tomato3' : '#CD4F39',
+ 'tomato4' : '#8B3626',
+ 'turquoise' : '#40E0D0',
+ 'turquoise1' : '#00F5FF',
+ 'turquoise2' : '#00E5EE',
+ 'turquoise3' : '#00C5CD',
+ 'turquoise4' : '#00868B',
+ 'violet' : '#EE82EE',
+ 'violetred' : '#D02090',
+ 'violetred1' : '#FF3E96',
+ 'violetred2' : '#EE3A8C',
+ 'violetred3' : '#CD3278',
+ 'violetred4' : '#8B2252',
+ 'webgray' : '#808080',
+ 'webgreen' : '#008000',
+ 'webgrey' : '#808080',
+ 'webmaroon' : '#800000',
+ 'webpurple' : '#800080',
+ 'wheat' : '#F5DEB3',
+ 'wheat1' : '#FFE7BA',
+ 'wheat2' : '#EED8AE',
+ 'wheat3' : '#CDBA96',
+ 'wheat4' : '#8B7E66',
+ 'white' : '#FFFFFF',
+ 'whitesmoke' : '#F5F5F5',
+ 'x11gray' : '#BEBEBE',
+ 'x11green' : '#00FF00',
+ 'x11grey' : '#BEBEBE',
+ 'x11maroon' : '#B03060',
+ 'x11purple' : '#A020F0',
+ 'yellow' : '#FFFF00',
+ 'yellow1' : '#FFFF00',
+ 'yellow2' : '#EEEE00',
+ 'yellow3' : '#CDCD00',
+ 'yellow4' : '#8B8B00',
+ 'yellowgreen' : '#9ACD32'
+ };
+
+ return ColorScheme;
+
+}]);
diff --git a/guacamole/src/main/webapp/app/index/styles/ui.css b/guacamole/src/main/webapp/app/index/styles/ui.css
index 58406eb..b90e271 100644
--- a/guacamole/src/main/webapp/app/index/styles/ui.css
+++ b/guacamole/src/main/webapp/app/index/styles/ui.css
@@ -198,6 +198,7 @@
background-image: url('images/protocol-icons/guac-plug.png');
}
+.connection .icon.kubernetes,
.connection .icon.ssh,
.connection .icon.telnet {
background-image: url('images/protocol-icons/guac-text.png');
diff --git a/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css b/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css
index 8fe19d6..c5645fb 100644
--- a/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css
+++ b/guacamole/src/main/webapp/app/manage/styles/connection-parameter.css
@@ -29,6 +29,7 @@
display: table;
padding-left: .5em;
border-left: 3px solid rgba(0,0,0,0.125);
+ width: 100%;
}
.connection-parameters .form .fields .labeled-field {
@@ -40,8 +41,11 @@
display: table-cell;
padding: 0.125em;
vertical-align: top;
+ width: 100%;
}
.connection-parameters .form .fields .field-header {
padding-right: 1em;
+ width: 0;
+ white-space: nowrap;
}
diff --git a/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js b/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js
index 23daef2..3f68924 100644
--- a/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js
+++ b/guacamole/src/main/webapp/app/navigation/types/ClientIdentifier.js
@@ -89,7 +89,14 @@
*
* @type String
*/
- CONNECTION_GROUP : 'g'
+ CONNECTION_GROUP : 'g',
+
+ /**
+ * The type string for an active Guacamole connection.
+ *
+ * @type String
+ */
+ ACTIVE_CONNECTION : 'a'
};
diff --git a/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js b/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js
index 5354bc1..3960b39 100644
--- a/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js
+++ b/guacamole/src/main/webapp/app/rest/services/activeConnectionService.js
@@ -30,6 +30,38 @@
var service = {};
/**
+ * Makes a request to the REST API to get a single active connection,
+ * returning a promise that provides the corresponding
+ * @link{ActiveConnection} if successful.
+ *
+ * @param {String} dataSource
+ * The identifier of the data source to retrieve the active connection
+ * from.
+ *
+ * @param {String} id
+ * The identifier of the active connection.
+ *
+ * @returns {Promise.<ActiveConnection>}
+ * A promise which will resolve with a @link{ActiveConnection} upon
+ * success.
+ */
+ service.getActiveConnection = function getActiveConnection(dataSource, id) {
+
+ // Build HTTP parameters set
+ var httpParameters = {
+ token : authenticationService.getCurrentToken()
+ };
+
+ // Retrieve active connection
+ return requestService({
+ method : 'GET',
+ url : 'api/session/data/' + encodeURIComponent(dataSource) + '/activeConnections/' + encodeURIComponent(id),
+ params : httpParameters
+ });
+
+ };
+
+ /**
* Makes a request to the REST API to get the list of active tunnels,
* returning a promise that provides a map of @link{ActiveConnection}
* objects if successful.
diff --git a/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js b/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js
index 6c5aac2..dc0b140 100644
--- a/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js
+++ b/guacamole/src/main/webapp/app/rest/types/ActiveConnection.js
@@ -76,6 +76,14 @@
*/
this.username = template.username;
+ /**
+ * Whether this active connection may be connected to, just as a
+ * normal connection.
+ *
+ * @type Boolean
+ */
+ this.connectable = template.connectable;
+
};
return ActiveConnection;
diff --git a/guacamole/src/main/webapp/app/rest/types/Field.js b/guacamole/src/main/webapp/app/rest/types/Field.js
index 84dfe13..195db82 100644
--- a/guacamole/src/main/webapp/app/rest/types/Field.js
+++ b/guacamole/src/main/webapp/app/rest/types/Field.js
@@ -168,7 +168,16 @@
*
* @type String
*/
- QUERY_PARAMETER : 'QUERY_PARAMETER'
+ QUERY_PARAMETER : 'QUERY_PARAMETER',
+
+ /**
+ * The type string associated with parameters that may contain color
+ * schemes accepted by the Guacamole server terminal emulator and
+ * protocols which leverage it.
+ *
+ * @type String
+ */
+ TERMINAL_COLOR_SCHEME : 'TERMINAL_COLOR_SCHEME'
};
diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js
index 5e1774d..30b8bde 100644
--- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js
+++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js
@@ -35,6 +35,7 @@
// Required types
var ActiveConnectionWrapper = $injector.get('ActiveConnectionWrapper');
+ var ClientIdentifier = $injector.get('ClientIdentifier');
var ConnectionGroup = $injector.get('ConnectionGroup');
var SortOrder = $injector.get('SortOrder');
@@ -336,7 +337,37 @@
'actions' : [ DELETE_ACTION, CANCEL_ACTION]
});
};
-
+
+ /**
+ * Returns the relative URL of the client page which accesses the
+ * given active connection. If the active connection is not
+ * connectable, null is returned.
+ *
+ * @param {String} dataSource
+ * The unique identifier of the data source containing the
+ * active connection.
+ *
+ * @param {String} activeConnection
+ * The active connection to determine the relative URL of.
+ *
+ * @returns {String}
+ * The relative URL of the client page which accesses the given
+ * active connection, or null if the active connection is not
+ * connectable.
+ */
+ $scope.getClientURL = function getClientURL(dataSource, activeConnection) {
+
+ if (!activeConnection.connectable)
+ return null;
+
+ return '#/client/' + encodeURIComponent(ClientIdentifier.toString({
+ dataSource : dataSource,
+ type : ClientIdentifier.Types.ACTIVE_CONNECTION,
+ id : activeConnection.identifier
+ }));
+
+ };
+
/**
* Returns whether the selected sessions can be deleted.
*
diff --git a/guacamole/src/main/webapp/app/settings/templates/settingsSessions.html b/guacamole/src/main/webapp/app/settings/templates/settingsSessions.html
index 184ff0b..698582c 100644
--- a/guacamole/src/main/webapp/app/settings/templates/settingsSessions.html
+++ b/guacamole/src/main/webapp/app/settings/templates/settingsSessions.html
@@ -40,7 +40,9 @@
<td><guac-user-item username="wrapper.activeConnection.username"></guac-user-item></td>
<td>{{wrapper.startDate}}</td>
<td>{{wrapper.activeConnection.remoteHost}}</td>
- <td>{{wrapper.name}}</td>
+ <td><a ng-href="{{
+ getClientURL(wrapper.dataSource, wrapper.activeConnection)
+ }}">{{wrapper.name}}</a></td>
</tr>
</tbody>
</table>
diff --git a/guacamole/src/main/webapp/index.html b/guacamole/src/main/webapp/index.html
index e675546..309f114 100644
--- a/guacamole/src/main/webapp/index.html
+++ b/guacamole/src/main/webapp/index.html
@@ -27,6 +27,7 @@
<link rel="icon" type="image/png" href="images/logo-64.png"/>
<link rel="icon" type="image/png" sizes="144x144" href="images/logo-144.png"/>
<link rel="apple-touch-icon" type="image/png" href="images/logo-144.png"/>
+ <link rel="stylesheet" type="text/css" href="webjars/simonwep__pickr/1.2.6/dist/themes/monolith.min.css"/>
<link rel="stylesheet" type="text/css" href="app.css?v=${project.version}">
<title ng-bind="page.title | translate"></title>
</head>
@@ -87,7 +88,10 @@
<!-- JSTZ -->
<script type="text/javascript" src="webjars/jstz/1.0.10/dist/jstz.min.js"></script>
-
+
+ <!-- Pickr (color picker) -->
+ <script type="text/javascript" src="webjars/simonwep__pickr/1.2.6/dist/pickr.es5.min.js"></script>
+
<!-- Polyfills for the "datalist" element, Blob and the FileSaver API -->
<script type="text/javascript" src="webjars/blob-polyfill/1.0.20150320/Blob.js"></script>
<script type="text/javascript" src="webjars/datalist-polyfill/1.14.0/datalist-polyfill.min.js"></script>
diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json
index 598fd9e..63deae5 100644
--- a/guacamole/src/main/webapp/translations/en.json
+++ b/guacamole/src/main/webapp/translations/en.json
@@ -150,6 +150,22 @@
},
+ "COLOR_SCHEME" : {
+
+ "ACTION_CANCEL" : "@:APP.ACTION_CANCEL",
+ "ACTION_HIDE_DETAILS" : "Hide",
+ "ACTION_SAVE" : "@:APP.ACTION_SAVE",
+ "ACTION_SHOW_DETAILS" : "Show",
+
+ "FIELD_HEADER_BACKGROUND" : "Background",
+ "FIELD_HEADER_FOREGROUND" : "Foreground",
+
+ "FIELD_OPTION_CUSTOM" : "Custom...",
+
+ "SECTION_HEADER_DETAILS" : "Details:"
+
+ },
+
"DATA_SOURCE_DEFAULT" : {
"NAME" : "Default (XML)"
},
@@ -364,7 +380,73 @@
"TEXT_CONFIRM_DELETE" : "Groups cannot be restored after they have been deleted. Are you sure you want to delete this group?"
},
-
+
+ "PROTOCOL_KUBERNETES" : {
+
+ "FIELD_HEADER_BACKSPACE" : "Backspace key sends:",
+ "FIELD_HEADER_CA_CERT" : "Certificate authority certificate:",
+ "FIELD_HEADER_CLIENT_CERT" : "Client certificate:",
+ "FIELD_HEADER_CLIENT_KEY" : "Client key:",
+ "FIELD_HEADER_COLOR_SCHEME" : "Color scheme:",
+ "FIELD_HEADER_CONTAINER" : "Container name:",
+ "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automatically create recording path:",
+ "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automatically create typescript path:",
+ "FIELD_HEADER_FONT_NAME" : "Font name:",
+ "FIELD_HEADER_FONT_SIZE" : "Font size:",
+ "FIELD_HEADER_HOSTNAME" : "Hostname:",
+ "FIELD_HEADER_IGNORE_CERT" : "Ignore server certificate:",
+ "FIELD_HEADER_NAMESPACE" : "Namespace:",
+ "FIELD_HEADER_POD" : "Pod name:",
+ "FIELD_HEADER_PORT" : "Port:",
+ "FIELD_HEADER_READ_ONLY" : "Read-only:",
+ "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclude mouse:",
+ "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclude graphics/streams:",
+ "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Include key events:",
+ "FIELD_HEADER_RECORDING_NAME" : "Recording name:",
+ "FIELD_HEADER_RECORDING_PATH" : "Recording path:",
+ "FIELD_HEADER_SCROLLBACK" : "Maximum scrollback size:",
+ "FIELD_HEADER_TYPESCRIPT_NAME" : "Typescript name:",
+ "FIELD_HEADER_TYPESCRIPT_PATH" : "Typescript path:",
+ "FIELD_HEADER_USE_SSL" : "Use SSL/TLS",
+
+ "FIELD_OPTION_BACKSPACE_EMPTY" : "",
+ "FIELD_OPTION_BACKSPACE_8" : "Backspace (Ctrl-H)",
+ "FIELD_OPTION_BACKSPACE_127" : "Delete (Ctrl-?)",
+
+ "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Black on white",
+ "FIELD_OPTION_COLOR_SCHEME_EMPTY" : "",
+ "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK" : "Gray on black",
+ "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Green on black",
+ "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "White on black",
+
+ "FIELD_OPTION_FONT_SIZE_8" : "8",
+ "FIELD_OPTION_FONT_SIZE_9" : "9",
+ "FIELD_OPTION_FONT_SIZE_10" : "10",
+ "FIELD_OPTION_FONT_SIZE_11" : "11",
+ "FIELD_OPTION_FONT_SIZE_12" : "12",
+ "FIELD_OPTION_FONT_SIZE_14" : "14",
+ "FIELD_OPTION_FONT_SIZE_18" : "18",
+ "FIELD_OPTION_FONT_SIZE_24" : "24",
+ "FIELD_OPTION_FONT_SIZE_30" : "30",
+ "FIELD_OPTION_FONT_SIZE_36" : "36",
+ "FIELD_OPTION_FONT_SIZE_48" : "48",
+ "FIELD_OPTION_FONT_SIZE_60" : "60",
+ "FIELD_OPTION_FONT_SIZE_72" : "72",
+ "FIELD_OPTION_FONT_SIZE_96" : "96",
+ "FIELD_OPTION_FONT_SIZE_EMPTY" : "",
+
+ "NAME" : "Kubernetes",
+
+ "SECTION_HEADER_AUTHENTICATION" : "Authentication",
+ "SECTION_HEADER_BEHAVIOR" : "Terminal behavior",
+ "SECTION_HEADER_CONTAINER" : "Container",
+ "SECTION_HEADER_DISPLAY" : "Display",
+ "SECTION_HEADER_RECORDING" : "Screen Recording",
+ "SECTION_HEADER_TYPESCRIPT" : "Typescript (Text Session Recording)",
+ "SECTION_HEADER_NETWORK" : "Network"
+
+ },
+
"PROTOCOL_RDP" : {
"FIELD_HEADER_CLIENT_NAME" : "Client name:",