GUACAMOLE-699: Merge changes bringing German translations up-to-date.

diff --git a/doc/guacamole-example/pom.xml b/doc/guacamole-example/pom.xml
index f2f56f3..bb2ea68 100644
--- a/doc/guacamole-example/pom.xml
+++ b/doc/guacamole-example/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-example</artifactId>
     <packaging>war</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-example</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -106,7 +106,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>compile</scope>
         </dependency>
 
@@ -114,7 +114,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common-js</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <type>zip</type>
             <scope>runtime</scope>
         </dependency>
diff --git a/doc/guacamole-playback-example/pom.xml b/doc/guacamole-playback-example/pom.xml
index 1b31374..f7055d3 100644
--- a/doc/guacamole-playback-example/pom.xml
+++ b/doc/guacamole-playback-example/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-playback-example</artifactId>
     <packaging>war</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-playback-example</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -88,7 +88,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common-js</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <type>zip</type>
             <scope>runtime</scope>
         </dependency>
diff --git a/extensions/guacamole-auth-cas/pom.xml b/extensions/guacamole-auth-cas/pom.xml
index ae8bd67..69ecab5 100644
--- a/extensions/guacamole-auth-cas/pom.xml
+++ b/extensions/guacamole-auth-cas/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-cas</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-cas</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -210,7 +210,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
@@ -218,7 +218,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java
index d769d2e..ceab174 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/AuthenticationProviderService.java
@@ -22,6 +22,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import java.util.Arrays;
+import java.util.Map;
 import javax.servlet.http.HttpServletRequest;
 import org.apache.guacamole.form.Field;
 import org.apache.guacamole.GuacamoleException;
@@ -31,7 +32,7 @@
 import org.apache.guacamole.auth.cas.conf.ConfigurationService;
 import org.apache.guacamole.auth.cas.form.CASTicketField;
 import org.apache.guacamole.auth.cas.ticket.TicketValidationService;
-import org.apache.guacamole.auth.cas.user.AuthenticatedUser;
+import org.apache.guacamole.auth.cas.user.CASAuthenticatedUser;
 
 /**
  * Service providing convenience functions for the CAS AuthenticationProvider
@@ -55,7 +56,7 @@
      * Provider for AuthenticatedUser objects.
      */
     @Inject
-    private Provider<AuthenticatedUser> authenticatedUserProvider;
+    private Provider<CASAuthenticatedUser> authenticatedUserProvider;
 
     /**
      * Returns an AuthenticatedUser representing the user authenticated by the
@@ -65,14 +66,14 @@
      *     The credentials to use for authentication.
      *
      * @return
-     *     An AuthenticatedUser representing the user authenticated by the
+     *     A CASAuthenticatedUser representing the user authenticated by the
      *     given credentials.
      *
      * @throws GuacamoleException
      *     If an error occurs while authenticating the user, or if access is
      *     denied.
      */
-    public AuthenticatedUser authenticateUser(Credentials credentials)
+    public CASAuthenticatedUser authenticateUser(Credentials credentials)
             throws GuacamoleException {
 
         // Pull CAS ticket from request if present
@@ -80,10 +81,11 @@
         if (request != null) {
             String ticket = request.getParameter(CASTicketField.PARAMETER_NAME);
             if (ticket != null) {
-                String username = ticketService.validateTicket(ticket, credentials);
+                Map<String, String> tokens = ticketService.validateTicket(ticket, credentials);
+                String username = credentials.getUsername();
                 if (username != null) {
-                    AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
-                    authenticatedUser.init(username, credentials);
+                    CASAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
+                    authenticatedUser.init(username, credentials, tokens);
                     return authenticatedUser;
                 }
             }
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/CASAuthenticationProvider.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/CASAuthenticationProvider.java
index ed51a31..5b4154e 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/CASAuthenticationProvider.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/CASAuthenticationProvider.java
@@ -22,9 +22,12 @@
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.cas.user.CASAuthenticatedUser;
 import org.apache.guacamole.net.auth.AbstractAuthenticationProvider;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
 import org.apache.guacamole.net.auth.Credentials;
+import org.apache.guacamole.net.auth.TokenInjectingUserContext;
+import org.apache.guacamole.net.auth.UserContext;
 
 /**
  * Guacamole authentication backend which authenticates users using an
@@ -71,5 +74,17 @@
         return authProviderService.authenticateUser(credentials);
 
     }
+    
+    @Override
+    public UserContext decorate(UserContext context,
+            AuthenticatedUser authenticatedUser, Credentials credentials)
+            throws GuacamoleException {
+        
+        if (!(authenticatedUser instanceof CASAuthenticatedUser))
+            return context;
+        
+        return new TokenInjectingUserContext(context,
+                ((CASAuthenticatedUser) authenticatedUser).getTokens());
+    }
 
 }
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java
index bfc3b69..ba7ac83 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java
@@ -28,13 +28,16 @@
 import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.NoSuchPaddingException;
 import java.nio.charset.Charset;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Map.Entry;
 import javax.xml.bind.DatatypeConverter;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleSecurityException;
 import org.apache.guacamole.GuacamoleServerException;
 import org.apache.guacamole.auth.cas.conf.ConfigurationService;
 import org.apache.guacamole.net.auth.Credentials;
-import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
-import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
+import org.apache.guacamole.token.TokenName;
 import org.jasig.cas.client.authentication.AttributePrincipal;
 import org.jasig.cas.client.validation.Assertion;
 import org.jasig.cas.client.validation.Cas20ProxyTicketValidator;
@@ -52,6 +55,11 @@
      * Logger for this class.
      */
     private static final Logger logger = LoggerFactory.getLogger(TicketValidationService.class);
+    
+    /**
+     * The prefix to use when generating token names.
+     */
+    public static final String CAS_ATTRIBUTE_TOKEN_PREFIX = "CAS_";
 
     /**
      * Service for retrieving CAS configuration information.
@@ -60,9 +68,9 @@
     private ConfigurationService confService;
 
     /**
-     * Validates and parses the given ID ticket, returning the username
-     * provided by the CAS server in the ticket.  If the
-     * ticket is invalid an exception is thrown.
+     * Validates and parses the given ID ticket, returning a map of all
+     * available tokens for the given user based on attributes provided by the
+     * CAS server.  If the ticket is invalid an exception is thrown.
      *
      * @param ticket
      *     The ID ticket to validate and parse.
@@ -72,13 +80,15 @@
      *     password values in.
      *
      * @return
-     *     The username derived from the ticket.
+     *     A Map all of tokens for the user parsed from attributes returned
+     *     by the CAS server.
      *
      * @throws GuacamoleException
      *     If the ID ticket is not valid or guacamole.properties could
      *     not be parsed.
      */
-    public String validateTicket(String ticket, Credentials credentials) throws GuacamoleException {
+    public Map<String, String> validateTicket(String ticket,
+            Credentials credentials) throws GuacamoleException {
 
         // Retrieve the configured CAS URL, establish a ticket validator,
         // and then attempt to validate the supplied ticket.  If that succeeds,
@@ -88,33 +98,43 @@
         validator.setAcceptAnyProxy(true);
         validator.setEncoding("UTF-8");
         try {
+            Map<String, String> tokens = new HashMap<>();
             String confRedirectURI = confService.getRedirectURI();
             Assertion a = validator.validate(ticket, confRedirectURI);
             AttributePrincipal principal =  a.getPrincipal();
+            Map<String, Object> ticketAttrs =
+                    new HashMap<>(principal.getAttributes());
 
             // Retrieve username and set the credentials.
             String username = principal.getName();
-            if (username != null)
-                credentials.setUsername(username);
+            if (username == null)
+                throw new GuacamoleSecurityException("No username provided by CAS.");
+            
+            credentials.setUsername(username);
 
             // Retrieve password, attempt decryption, and set credentials.
-            Object credObj = principal.getAttributes().get("credential");
+            Object credObj = ticketAttrs.remove("credential");
             if (credObj != null) {
                 String clearPass = decryptPassword(credObj.toString());
                 if (clearPass != null && !clearPass.isEmpty())
                     credentials.setPassword(clearPass);
             }
+            
+            // Convert remaining attributes that have values to Strings
+            for (Entry <String, Object> attr : ticketAttrs.entrySet()) {
+                String tokenName = TokenName.canonicalize(attr.getKey(),
+                        CAS_ATTRIBUTE_TOKEN_PREFIX);
+                Object value = attr.getValue();
+                if (value != null)
+                    tokens.put(tokenName, value.toString());
+            }
 
-            return username;
+            return tokens;
 
         } 
         catch (TicketValidationException e) {
             throw new GuacamoleException("Ticket validation failed.", e);
         }
-        catch (Throwable t) {
-            logger.error("Error validating ticket with CAS server: {}", t.getMessage());
-            throw new GuacamoleInvalidCredentialsException("CAS login failed.", CredentialsInfo.USERNAME_PASSWORD);
-        }
 
     }
 
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/AuthenticatedUser.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/CASAuthenticatedUser.java
similarity index 61%
rename from extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/AuthenticatedUser.java
rename to extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/CASAuthenticatedUser.java
index 081971e..1b3a948 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/AuthenticatedUser.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/user/CASAuthenticatedUser.java
@@ -20,6 +20,8 @@
 package org.apache.guacamole.auth.cas.user;
 
 import com.google.inject.Inject;
+import java.util.Collections;
+import java.util.Map;
 import org.apache.guacamole.net.auth.AbstractAuthenticatedUser;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
 import org.apache.guacamole.net.auth.Credentials;
@@ -29,7 +31,7 @@
  * username and particular set of credentials with the CAS authentication
  * provider.
  */
-public class AuthenticatedUser extends AbstractAuthenticatedUser {
+public class CASAuthenticatedUser extends AbstractAuthenticatedUser {
 
     /**
      * Reference to the authentication provider associated with this
@@ -42,10 +44,15 @@
      * The credentials provided when this user was authenticated.
      */
     private Credentials credentials;
+    
+    /**
+     * Tokens associated with this authenticated user.
+     */
+    private Map<String, String> tokens;
 
     /**
      * Initializes this AuthenticatedUser using the given username and
-     * credentials.
+     * credentials, and an empty map of parameter tokens.
      *
      * @param username
      *     The username of the user that was authenticated.
@@ -54,10 +61,42 @@
      *     The credentials provided when this user was authenticated.
      */
     public void init(String username, Credentials credentials) {
+        this.init(username, credentials, Collections.emptyMap());
+    }
+    
+    /**
+     * Initializes this AuthenticatedUser using the given username,
+     * credentials, and parameter tokens.
+     *
+     * @param username
+     *     The username of the user that was authenticated.
+     *
+     * @param credentials
+     *     The credentials provided when this user was authenticated.
+     * 
+     * @param tokens
+     *     A map of all the name/value pairs that should be available
+     *     as tokens when connections are established with this user.
+     */
+    public void init(String username, Credentials credentials,
+            Map<String, String> tokens) {
         this.credentials = credentials;
+        this.tokens = Collections.unmodifiableMap(tokens);
         setIdentifier(username.toLowerCase());
     }
 
+    /**
+     * Returns a Map containing the name/value pairs that can be applied
+     * as parameter tokens when connections are established by the user.
+     * 
+     * @return
+     *     A Map containing all of the name/value pairs that can be
+     *     used as parameter tokens by this user.
+     */
+    public Map<String, String> getTokens() {
+        return tokens;
+    }
+
     @Override
     public AuthenticationProvider getAuthenticationProvider() {
         return authProvider;
diff --git a/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
index d261fdd..549bba9 100644
--- a/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "CAS Authentication Extension",
     "namespace" : "guac-cas",
diff --git a/extensions/guacamole-auth-duo/pom.xml b/extensions/guacamole-auth-duo/pom.xml
index 55de752..330f76e 100644
--- a/extensions/guacamole-auth-duo/pom.xml
+++ b/extensions/guacamole-auth-duo/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-duo</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-duo</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -213,7 +213,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
index 2f0f8f3..efb0dda 100644
--- a/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "Duo TFA Authentication Backend",
     "namespace" : "duo",
diff --git a/extensions/guacamole-auth-header/pom.xml b/extensions/guacamole-auth-header/pom.xml
index 9c07bab..cf65658 100644
--- a/extensions/guacamole-auth-header/pom.xml
+++ b/extensions/guacamole-auth-header/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-header</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-header</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -130,7 +130,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-header/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-header/src/main/resources/guac-manifest.json
index c976eef..3c71028 100644
--- a/extensions/guacamole-auth-header/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-header/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "HTTP Header Authentication Extension",
     "namespace" : "guac-header",
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
index a3b7f92..fdc8003 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.0.0</version>
+        <version>1.1.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java
index 68e2a47..ff605b9 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/JDBCAuthenticationProviderService.java
@@ -96,6 +96,7 @@
 
         // Retrieve user account for already-authenticated user
         ModeledUser user = userService.retrieveUser(authenticationProvider, authenticatedUser);
+        ModeledUserContext context = userContextProvider.get();
         if (user != null && !user.isDisabled()) {
 
             // Enforce applicable account restrictions
@@ -118,24 +119,23 @@
                     userService.resetExpiredPassword(user, authenticatedUser.getCredentials());
             }
 
-            // Return all data associated with the authenticated user
-            ModeledUserContext context = userContextProvider.get();
-            context.init(user.getCurrentUser());
-            return context;
-
+        }
+        
+        // If no user account is found, and database-specific account
+        // restrictions do not apply, get an empty user.
+        else if (!databaseRestrictionsApplicable) {
+            user = userService.retrieveSkeletonUser(authenticationProvider, authenticatedUser);
         }
 
         // Veto authentication result only if database-specific account
         // restrictions apply in this situation
-        if (databaseRestrictionsApplicable)
+        else
             throw new GuacamoleInvalidCredentialsException("Invalid login",
                     CredentialsInfo.USERNAME_PASSWORD);
-
-        // There is no data to be returned for the user, either because they do
-        // not exist or because restrictions prevent their data from being
-        // retrieved, but no restrictions apply which should prevent the user
-        // from authenticating entirely
-        return null;
+        
+        // Initialize the UserContext with the user account and return it.
+        context.init(user.getCurrentUser());
+        return context;
 
     }
 
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-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledAuthenticatedUser.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledAuthenticatedUser.java
index 828b05e..5778ad0 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledAuthenticatedUser.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/ModeledAuthenticatedUser.java
@@ -19,6 +19,7 @@
 
 package org.apache.guacamole.auth.jdbc.user;
 
+import com.google.common.collect.Sets;
 import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
@@ -168,5 +169,11 @@
     public void setIdentifier(String identifier) {
         user.setIdentifier(identifier);
     }
+    
+    @Override
+    public Set<String> getEffectiveUserGroups() {
+        return Sets.union(user.getEffectiveUserGroups(),
+                super.getEffectiveUserGroups());
+    }
 
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserModel.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserModel.java
index 194a26d..3d441d6 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserModel.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserModel.java
@@ -127,6 +127,17 @@
     public UserModel() {
         super(EntityType.USER);
     }
+    
+    /**
+     * Creates a new user having the provided identifier.
+     * 
+     * @param identifier
+     *     The identifier of the new user.
+     */
+    public UserModel(String identifier) {
+        super(EntityType.USER);
+        super.setIdentifier(identifier);
+    }
 
     /**
      * Returns the hash of this user's password and password salt. This may be
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java
index 60bd1e1..0cfe900 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/UserService.java
@@ -423,6 +423,43 @@
         return user;
 
     }
+    
+    /**
+     * Generates an empty (skeleton) user corresponding to the given
+     * AuthenticatedUser.  The user will not be stored in the database, and
+     * will only be available in-memory during the time the session is
+     * active.
+     * 
+     * @param authenticationProvider
+     *     The AuthenticationProvider on behalf of which the user is being
+     *     retrieved.
+     *
+     * @param authenticatedUser
+     *     The AuthenticatedUser to generate the skeleton account for.
+     *
+     * @return
+     *     The empty ModeledUser which corresponds to the given
+     *     AuthenticatedUser.
+     *
+     * @throws GuacamoleException
+     *     If a ModeledUser object for the user corresponding to the given
+     *     AuthenticatedUser cannot be created.
+     */
+    public ModeledUser retrieveSkeletonUser(AuthenticationProvider authenticationProvider,
+            AuthenticatedUser authenticatedUser) throws GuacamoleException {
+        
+        // Set up an empty user model
+        ModeledUser user = getObjectInstance(null,
+                new UserModel(authenticatedUser.getIdentifier()));
+        
+        // Create user object, and configure cyclic reference
+        user.setCurrentUser(new ModeledAuthenticatedUser(authenticatedUser,
+                authenticationProvider, user));
+        
+        // Return the new user.
+        return user;
+        
+    }
 
     /**
      * Resets the password of the given user to the new password specified via
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/pom.xml
index a204c8e..ddb57a9 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.0.0</version>
+        <version>1.1.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -99,21 +99,21 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-mysql</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
         <!-- PostgreSQL Authentication Extension -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-postgresql</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
         <!-- SQL Server Authentication Extension -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-sqlserver</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml
index b438735..8a7d68a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.0.0</version>
+        <version>1.1.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -120,7 +120,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-base</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guac-manifest.json
index c9310c6..afd45ee 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "MySQL Authentication",
     "namespace" : "guac-mysql",
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/base/EntityMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/base/EntityMapper.xml
index 21efb99..a292511 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/base/EntityMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/base/EntityMapper.xml
@@ -73,27 +73,31 @@
             JOIN guacamole_user_group_member ON guacamole_user_group.user_group_id = guacamole_user_group_member.user_group_id
             WHERE
                 guacamole_user_group.disabled = false
-                AND (
-                    guacamole_user_group_member.member_entity_id = #{entity.entityID}
-                    <if test="!effectiveGroups.isEmpty()">
-                        OR guacamole_user_group_member.member_entity_id IN (
-                            SELECT entity_id FROM guacamole_entity
-                            WHERE type = 'USER_GROUP' AND name IN
-                                <foreach collection="effectiveGroups" item="effectiveGroup"
-                                         open="(" separator="," close=")">
-                                    #{effectiveGroup,jdbcType=VARCHAR}
-                                </foreach>
-                        )
-                        OR guacamole_user_group.entity_id IN (
-                            SELECT entity_id FROM guacamole_entity
-                            WHERE type = 'USER_GROUP' AND name IN
-                                <foreach collection="effectiveGroups" item="effectiveGroup"
-                                         open="(" separator="," close=")">
-                                    #{effectiveGroup,jdbcType=VARCHAR}
-                                </foreach>
-                        )
-                    </if>
-                )
+                AND guacamole_user_group_member.member_entity_id = #{entity.entityID}
+            <if test="!effectiveGroups.isEmpty()">
+                UNION SELECT
+                    guacamole_entity.name
+                FROM guacamole_user_group
+                JOIN guacamole_entity ON guacamole_user_group.entity_id = guacamole_entity.entity_id
+                JOIN guacamole_user_group_member ON guacamole_user_group.user_group_id = guacamole_user_group_member.user_group_id
+                JOIN guacamole_entity member_entity ON guacamole_user_group_member.member_entity_id = member_entity.entity_id
+                WHERE
+                    guacamole_user_group.disabled = false
+                    AND member_entity.type = 'USER_GROUP' AND member_entity.name IN
+                        <foreach collection="effectiveGroups" item="effectiveGroup"
+                                 open="(" separator="," close=")">
+                            #{effectiveGroup,jdbcType=VARCHAR}
+                        </foreach>
+                UNION SELECT
+                    guacamole_entity.name
+                FROM guacamole_user_group
+                JOIN guacamole_entity ON guacamole_user_group.entity_id = guacamole_entity.entity_id
+                WHERE type = 'USER_GROUP' AND name IN
+                    <foreach collection="effectiveGroups" item="effectiveGroup"
+                             open="(" separator="," close=")">
+                        #{effectiveGroup,jdbcType=VARCHAR}
+                    </foreach>
+            </if>
         </if>
 
         <if test="recursive">
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml
index bfe4a3c..340eba7 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.0.0</version>
+        <version>1.1.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -120,7 +120,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-base</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.json
index b4cce47..22d894f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "PostgreSQL Authentication",
     "namespace" : "guac-postgresql",
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml
index 0a8468b..55455db 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.0.0</version>
+        <version>1.1.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -120,7 +120,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-base</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/guac-manifest.json
index 0641c65..6e7143c 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "SQLServer Authentication",
     "namespace" : "guac-sqlserver",
diff --git a/extensions/guacamole-auth-jdbc/pom.xml b/extensions/guacamole-auth-jdbc/pom.xml
index 9aee2df..1325949 100644
--- a/extensions/guacamole-auth-jdbc/pom.xml
+++ b/extensions/guacamole-auth-jdbc/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-jdbc</artifactId>
     <packaging>pom</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-jdbc</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -81,7 +81,7 @@
             <dependency>
                 <groupId>org.apache.guacamole</groupId>
                 <artifactId>guacamole-ext</artifactId>
-                <version>1.0.0</version>
+                <version>1.1.0</version>
                 <scope>provided</scope>
             </dependency>
 
diff --git a/extensions/guacamole-auth-ldap/pom.xml b/extensions/guacamole-auth-ldap/pom.xml
index 856f630..17c8ed6 100644
--- a/extensions/guacamole-auth-ldap/pom.xml
+++ b/extensions/guacamole-auth-ldap/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-ldap</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-ldap</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -137,15 +137,21 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <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 -->
@@ -160,14 +166,6 @@
             <version>3.0</version>
         </dependency>
 
-        <!-- JUnit -->
-        <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <version>4.12</version>
-            <scope>test</scope>
-        </dependency>
-
     </dependencies>
 
 </project>
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 d9d3ab1..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,24 +21,27 @@
 
 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.slf4j.Logger;
@@ -53,7 +56,12 @@
     /**
      * Logger for this class.
      */
-    private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
+    private static final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
+    
+    /**
+     * The prefix that will be used when generating tokens.
+     */
+    public static final String LDAP_ATTRIBUTE_TOKEN_PREFIX = "LDAP_";
 
     /**
      * Service for creating and managing connections to LDAP servers.
@@ -107,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()
             );
@@ -130,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;
 
@@ -158,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.
@@ -222,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;
 
     }
 
@@ -280,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();
@@ -292,28 +253,27 @@
 
         // Build LDAP query parameters
         String[] attrArray = attrList.toArray(new String[attrList.size()]);
-        String userDN = getUserBindDN(username);
 
-        Map<String, String> tokens = new HashMap<String, String>();
+        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.fromAttribute(attr.getName()), 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);
         }
 
@@ -340,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 f849126..7141a79 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,15 +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.ReferralAuthHandler;
+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;
 
@@ -40,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.
@@ -49,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) {
@@ -69,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.
@@ -107,86 +125,105 @@
      * @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);
-
+        // Get ldapConnection and try to connect and bind.
+        LdapNetworkConnection ldapConnection = createLDAPConnection();
         try {
 
             // 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) {
+            ldapConnection.close();
+            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);
+        
     }
 
     /**
@@ -196,19 +233,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 e605b3c..0000000
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java
+++ /dev/null
@@ -1,76 +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.google.inject.Inject;
-import com.novell.ldap.LDAPAuthHandler;
-import com.novell.ldap.LDAPAuthProvider;
-import com.novell.ldap.LDAPConnection;
-import java.io.UnsupportedEncodingException;
-import org.apache.guacamole.GuacamoleException;
-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.
-     */
-    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 6a96d5b..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);
+                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-ldap/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json
index 52b2d41..d619295 100644
--- a/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "LDAP Authentication",
     "namespace" : "guac-ldap",
diff --git a/extensions/guacamole-auth-ldap/src/test/java/org/apache/guacamole/auth/ldap/TokenNameTest.java b/extensions/guacamole-auth-ldap/src/test/java/org/apache/guacamole/auth/ldap/TokenNameTest.java
deleted file mode 100644
index 2ba61dc..0000000
--- a/extensions/guacamole-auth-ldap/src/test/java/org/apache/guacamole/auth/ldap/TokenNameTest.java
+++ /dev/null
@@ -1,53 +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 static org.junit.Assert.assertEquals;
-import org.junit.Test;
-
-/**
- * Test which verifies automatic generation of LDAP-specific connection
- * parameter token names.
- */
-public class TokenNameTest {
-
-    /**
-     * Verifies that TokenName.fromAttribute() generates token names as
-     * specified, regardless of the naming convention of the attribute.
-     */
-    @Test
-    public void testFromAttribute() {
-        assertEquals("LDAP_A", TokenName.fromAttribute("a"));
-        assertEquals("LDAP_B", TokenName.fromAttribute("b"));
-        assertEquals("LDAP_1", TokenName.fromAttribute("1"));
-        assertEquals("LDAP_SOME_URL", TokenName.fromAttribute("someURL"));
-        assertEquals("LDAP_LOWERCASE_WITH_DASHES", TokenName.fromAttribute("lowercase-with-dashes"));
-        assertEquals("LDAP_HEADLESS_CAMEL_CASE", TokenName.fromAttribute("headlessCamelCase"));
-        assertEquals("LDAP_CAMEL_CASE", TokenName.fromAttribute("CamelCase"));
-        assertEquals("LDAP_CAMEL_CASE", TokenName.fromAttribute("CamelCase"));
-        assertEquals("LDAP_LOWERCASE_WITH_UNDERSCORES", TokenName.fromAttribute("lowercase_with_underscores"));
-        assertEquals("LDAP_UPPERCASE_WITH_UNDERSCORES", TokenName.fromAttribute("UPPERCASE_WITH_UNDERSCORES"));
-        assertEquals("LDAP_A_VERY_INCONSISTENT_MIX_OF_ALL_STYLES", TokenName.fromAttribute("aVery-INCONSISTENTMix_ofAllStyles"));
-        assertEquals("LDAP_ABC_123_DEF_456", TokenName.fromAttribute("abc123def456"));
-        assertEquals("LDAP_ABC_123_DEF_456", TokenName.fromAttribute("ABC123DEF456"));
-        assertEquals("LDAP_WORD_A_WORD_AB_WORD_ABC_WORD", TokenName.fromAttribute("WordAWordABWordABCWord"));
-    }
-
-}
diff --git a/extensions/guacamole-auth-openid/pom.xml b/extensions/guacamole-auth-openid/pom.xml
index 63ecd51..967ad03 100644
--- a/extensions/guacamole-auth-openid/pom.xml
+++ b/extensions/guacamole-auth-openid/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-openid</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-openid</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -210,7 +210,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
index f81f602..c517be1 100644
--- a/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "OpenID Authentication Extension",
     "namespace" : "guac-openid",
diff --git a/extensions/guacamole-auth-quickconnect/pom.xml b/extensions/guacamole-auth-quickconnect/pom.xml
index 8acf5f6..df290b8 100644
--- a/extensions/guacamole-auth-quickconnect/pom.xml
+++ b/extensions/guacamole-auth-quickconnect/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-quickconnect</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-quickconnect</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -188,7 +188,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
index 8ec1605..eca4d94 100644
--- a/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
@@ -1,5 +1,5 @@
 {
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"             : "Adhoc Guacamole Connections",
     "namespace"        : "quickconnect",
diff --git a/extensions/guacamole-auth-radius/pom.xml b/extensions/guacamole-auth-radius/pom.xml
index d07a60a..73c332a 100644
--- a/extensions/guacamole-auth-radius/pom.xml
+++ b/extensions/guacamole-auth-radius/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-radius</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-radius</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -178,7 +178,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
@@ -186,7 +186,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
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 4d3bbea..304a87d 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "RADIUS Authentication Backend",
     "namespace" : "radius",
diff --git a/extensions/guacamole-auth-totp/pom.xml b/extensions/guacamole-auth-totp/pom.xml
index d34db07..11199df 100644
--- a/extensions/guacamole-auth-totp/pom.xml
+++ b/extensions/guacamole-auth-totp/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-totp</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-totp</name>
     <url>http://guacamole.incubator.apache.org/</url>
 
@@ -217,7 +217,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
index e57a0e7..6845286 100644
--- a/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "TOTP TFA Authentication Backend",
     "namespace" : "totp",
diff --git a/guacamole-common-js/pom.xml b/guacamole-common-js/pom.xml
index cf9c45d..e94271b 100644
--- a/guacamole-common-js/pom.xml
+++ b/guacamole-common-js/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-common-js</artifactId>
     <packaging>pom</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-common-js</name>
     <url>http://guacamole.apache.org/</url>
 
diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js
index e947b70..07baf77 100644
--- a/guacamole-common-js/src/main/webapp/modules/Client.js
+++ b/guacamole-common-js/src/main/webapp/modules/Client.js
@@ -650,6 +650,23 @@
     this.onvideo = null;
 
     /**
+     * Fired when the current value of a connection parameter is being exposed
+     * by the server.
+     *
+     * @event
+     * @param {Guacamole.InputStream} stream
+     *     The stream that will receive connection parameter data from the
+     *     server.
+     *
+     * @param {String} mimetype
+     *     The mimetype of the data which will be received.
+     *
+     * @param {String} name
+     *     The name of the connection parameter whose value is being exposed.
+     */
+    this.onargv = null;
+
+    /**
      * Fired when the clipboard of the remote client is changing.
      * 
      * @event
@@ -846,6 +863,24 @@
 
         },
 
+        "argv": function(parameters) {
+
+            var stream_index = parseInt(parameters[0]);
+            var mimetype = parameters[1];
+            var name = parameters[2];
+
+            // Create stream
+            if (guac_client.onargv) {
+                var stream = streams[stream_index] = new Guacamole.InputStream(guac_client, stream_index);
+                guac_client.onargv(stream, mimetype, name);
+            }
+
+            // Otherwise, unsupported
+            else
+                guac_client.sendAck(stream_index, "Receiving argument values unsupported", 0x0100);
+
+        },
+
         "audio": function(parameters) {
 
             var stream_index = parseInt(parameters[0]);
diff --git a/guacamole-common-js/src/main/webapp/modules/Version.js b/guacamole-common-js/src/main/webapp/modules/Version.js
index 737d8d2..9131165 100644
--- a/guacamole-common-js/src/main/webapp/modules/Version.js
+++ b/guacamole-common-js/src/main/webapp/modules/Version.js
@@ -27,4 +27,4 @@
  *
  * @type {String}
  */
-Guacamole.API_VERSION = "1.0.0";
+Guacamole.API_VERSION = "1.1.0";
diff --git a/guacamole-common/pom.xml b/guacamole-common/pom.xml
index 219d694..2b4f353 100644
--- a/guacamole-common/pom.xml
+++ b/guacamole-common/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-common</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-common</name>
     <url>http://guacamole.apache.org/</url>
 
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java
index cf43b68..fe4efca 100644
--- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java
@@ -19,7 +19,6 @@
 
 package org.apache.guacamole.protocol;
 
-
 import java.util.List;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
@@ -57,6 +56,15 @@
     private String id;
     
     /**
+     * The protocol version that will be used to communicate with guacd.  The
+     * default is 1.0.0, and, if the server does not provide a specific version
+     * it will be assumed that it operates at this version and certain features
+     * may be unavailable.
+     */
+    private GuacamoleProtocolVersion protocolVersion =
+            GuacamoleProtocolVersion.VERSION_1_0_0;
+    
+    /**
      * Waits for the instruction having the given opcode, returning that
      * instruction once it has been read. If the instruction is never read,
      * an exception is thrown.
@@ -142,6 +150,23 @@
 
             // Retrieve argument name
             String arg_name = arg_names.get(i);
+            
+            // Check for valid protocol version as first argument
+            if (i == 0) {
+                GuacamoleProtocolVersion version = GuacamoleProtocolVersion.parseVersion(arg_name);
+                if (version != null) {
+
+                    // Use the lowest common version supported
+                    if (version.atLeast(GuacamoleProtocolVersion.LATEST))
+                        version = GuacamoleProtocolVersion.LATEST;
+
+                    // Respond with the version selected
+                    arg_values[i] = version.toString();
+                    protocolVersion = version;
+                    continue;
+
+                }
+            }
 
             // Get defined value for name
             String value = config.getParameter(arg_name);
@@ -184,6 +209,13 @@
                     "image",
                     info.getImageMimetypes().toArray(new String[0])
                 ));
+        
+        // Send client timezone, if supported and available
+        if (GuacamoleProtocolCapability.TIMEZONE_HANDSHAKE.isSupported(protocolVersion)) {
+            String timezone = info.getTimezone();
+            if (timezone != null)
+                writer.writeInstruction(new GuacamoleInstruction("timezone", info.getTimezone()));
+        }
 
         // Send args
         writer.writeInstruction(new GuacamoleInstruction("connect", arg_values));
@@ -221,6 +253,20 @@
         return id;
     }
 
+    /**
+     * Returns the version of the Guacamole protocol associated with the
+     * Guacamole connection negotiated by this ConfiguredGuacamoleSocket. This
+     * version is the lowest version common to both ConfiguredGuacamoleSocket
+     * and the relevant Guacamole proxy instance (guacd).
+     *
+     * @return
+     *     The protocol version that this ConfiguredGuacamoleSocket will use to
+     *     communicate with guacd.
+     */
+    public GuacamoleProtocolVersion getProtocolVersion() {
+        return protocolVersion;
+    }
+
     @Override
     public GuacamoleWriter getWriter() {
         return socket.getWriter();
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleClientInformation.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleClientInformation.java
index d90d05d..6d54a2f 100644
--- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleClientInformation.java
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleClientInformation.java
@@ -58,6 +58,11 @@
      * The list of image mimetypes reported by the client to be supported.
      */
     private final List<String> imageMimetypes = new ArrayList<String>();
+    
+    /**
+     * The timezone reported by the client.
+     */
+    private String timezone;
 
     /**
      * Returns the optimal screen width requested by the client, in pixels.
@@ -144,5 +149,31 @@
     public List<String> getImageMimetypes() {
         return imageMimetypes;
     }
+    
+    /**
+     * Return the timezone as reported by the client, or null if the timezone
+     * is not set.  Valid timezones are specified in IANA zone key format,
+     * also known as Olson time zone database or TZ Database.
+     * 
+     * @return
+     *     A string value of the timezone reported by the client.
+     */
+    public String getTimezone() {
+        return timezone;
+    }
+    
+    /**
+     * Set the string value of the timezone, or null if the timezone will not
+     * be provided by the client.  Valid timezones are specified in IANA zone
+     * key format (aka Olson time zone database or tz database).
+     * 
+     * @param timezone
+     *     The string value of the timezone reported by the client, in tz
+     *     database format, or null if the timezone is not provided by the
+     *     client.
+     */
+    public void setTimezone(String timezone) {
+        this.timezone = timezone;
+    }
 
 }
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolCapability.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolCapability.java
new file mode 100644
index 0000000..79f73f8
--- /dev/null
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolCapability.java
@@ -0,0 +1,85 @@
+/*
+ * 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.protocol;
+
+/**
+ * Capabilities which may not be present in all versions of the Guacamole
+ * protocol.
+ */
+public enum GuacamoleProtocolCapability {
+    
+    /**
+     * The protocol does not require handshake instructions to be sent in a
+     * specific order, nor that all handshake instructions be sent. Arbitrary
+     * handshake order was introduced in
+     * {@link GuacamoleProtocolVersion#VERSION_1_1_0}.
+     */
+    ARBITRARY_HANDSHAKE_ORDER(GuacamoleProtocolVersion.VERSION_1_1_0),
+    
+    /**
+     * Negotiation of Guacamole protocol version between client and server
+     * during the protocol handshake. The ability to negotiate protocol
+     * versions was introduced in
+     * {@link GuacamoleProtocolVersion#VERSION_1_1_0}.
+     */
+    PROTOCOL_VERSION_DETECTION(GuacamoleProtocolVersion.VERSION_1_1_0),
+    
+    /**
+     * Support for the "timezone" handshake instruction. The "timezone"
+     * instruction allows the client to request that the server forward their
+     * local timezone for use within the remote desktop session. Support for
+     * forwarding the client timezone was introduced in
+     * {@link GuacamoleProtocolVersion#VERSION_1_1_0}.
+     */
+    TIMEZONE_HANDSHAKE(GuacamoleProtocolVersion.VERSION_1_1_0);
+    
+    /**
+     * The minimum protocol version required to support this capability.
+     */
+    private final GuacamoleProtocolVersion version;
+    
+    /**
+     * Create a new enum value with the given protocol version as the minimum
+     * required to support the capability.
+     * 
+     * @param version
+     *     The minimum required protocol version for supporting the
+     *     capability.
+     */
+    private GuacamoleProtocolCapability(GuacamoleProtocolVersion version) {
+        this.version = version;
+    }
+
+    /**
+     * Returns whether this capability is supported in the given Guacamole
+     * protocol version.
+     *
+     * @param version
+     *     The Guacamole protocol version to check.
+     *
+     * @return
+     *     true if this capability is supported by the given protocol version,
+     *     false otherwise.
+     */
+    public boolean isSupported(GuacamoleProtocolVersion version) {
+        return version.atLeast(this.version);
+    }
+
+}
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolVersion.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolVersion.java
new file mode 100644
index 0000000..c1d50ba
--- /dev/null
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/GuacamoleProtocolVersion.java
@@ -0,0 +1,211 @@
+/*
+ * 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.protocol;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Representation of a Guacamole protocol version. Convenience methods are
+ * provided for parsing and comparing versions, as is necessary when
+ * determining the version of the Guacamole protocol common to guacd and a
+ * client.
+ */
+public class GuacamoleProtocolVersion {
+    
+    /**
+     * Protocol version 1.0.0 and older.  Any client that doesn't explicitly
+     * set the protocol version will negotiate down to this protocol version.
+     * This requires that handshake instructions be ordered correctly, and
+     * lacks support for certain protocol-related features introduced in later
+     * versions.
+     */
+    public static final GuacamoleProtocolVersion VERSION_1_0_0 = new GuacamoleProtocolVersion(1, 0, 0);
+
+    /**
+     * Protocol version 1.1.0, which introduces Client-Server version
+     * detection, arbitrary handshake instruction order, and support
+     * for passing the client timezone to the server during the handshake.
+     */
+    public static final GuacamoleProtocolVersion VERSION_1_1_0 = new GuacamoleProtocolVersion(1, 1, 0);
+
+    /**
+     * The most recent version of the Guacamole protocol at the time this
+     * version of GuacamoleProtocolVersion was built.
+     */
+    public static final GuacamoleProtocolVersion LATEST = VERSION_1_1_0;
+    
+    /**
+     * A regular expression that matches the VERSION_X_Y_Z pattern, where
+     * X is the major version component, Y is the minor version component,
+     * and Z is the patch version component.  This expression puts each of
+     * the version components in their own group so that they can be easily
+     * used later.
+     */
+    private static final Pattern VERSION_PATTERN =
+            Pattern.compile("^VERSION_([0-9]+)_([0-9]+)_([0-9]+)$");
+    
+    /**
+     * The major version component of the protocol version.
+     */
+    private final int major;
+
+    /**
+     * The minor version component of the protocol version.
+     */
+    private final int minor;
+
+    /**
+     * The patch version component of the protocol version.
+     */
+    private final int patch;
+    
+    /**
+     * Generate a new GuacamoleProtocolVersion object with the given
+     * major version, minor version, and patch version.
+     * 
+     * @param major
+     *     The integer representation of the major version component.
+     * 
+     * @param minor
+     *     The integer representation of the minor version component.
+     * 
+     * @param patch 
+     *     The integer representation of the patch version component.
+     */
+    public GuacamoleProtocolVersion(int major, int minor, int patch) {
+        this.major = major;
+        this.minor = minor;
+        this.patch = patch;
+    }
+    
+    /**
+     * Return the major version component of the protocol version.
+     * 
+     * @return 
+     *     The integer major version component.
+     */
+    public int getMajor() {
+        return major;
+    }
+    
+    /**
+     * Return the minor version component of the protocol version.
+     * 
+     * @return 
+     *     The integer minor version component.
+     */
+    public int getMinor() {
+        return minor;
+    }
+    
+    /**
+     * Return the patch version component of the protocol version.
+     * 
+     * @return 
+     *     The integer patch version component.
+     */
+    public int getPatch() {
+        return patch;
+    }
+    
+    /**
+     * Returns whether this GuacamoleProtocolVersion is at least as recent as
+     * (greater than or equal to) the given version.
+     *
+     * @param otherVersion
+     *     The version to which this GuacamoleProtocolVersion should be compared.
+     * 
+     * @return 
+     *     true if this object is at least as recent as the given version,
+     *     false if the given version is newer.
+     */
+    public boolean atLeast(GuacamoleProtocolVersion otherVersion) {
+        
+        // If major is not the same, return inequality
+        if (major != otherVersion.getMajor())
+            return this.major > otherVersion.getMajor();
+        
+        // Major is the same, but minor is not, return minor inequality
+        if (minor != otherVersion.getMinor())
+            return this.minor > otherVersion.getMinor();
+        
+        // Major and minor are equal, so return patch inequality
+        return patch >= otherVersion.getPatch();
+        
+    }
+    
+    /**
+     * Parse the String format of the version provided and return the
+     * the enum value matching that version.  If no value is provided, return
+     * null.
+     * 
+     * @param version
+     *     The String format of the version to parse.
+     * 
+     * @return
+     *     The enum value that matches the specified version, VERSION_1_0_0
+     *     if no match is found, or null if no comparison version is provided.
+     */
+    public static GuacamoleProtocolVersion parseVersion(String version) {
+
+        // Validate format of version string
+        Matcher versionMatcher = VERSION_PATTERN.matcher(version);
+        if (!versionMatcher.matches())
+            return null;
+
+        // Parse version number from version string
+        return new GuacamoleProtocolVersion(
+            Integer.parseInt(versionMatcher.group(1)),
+            Integer.parseInt(versionMatcher.group(2)),
+            Integer.parseInt(versionMatcher.group(3))
+        );
+
+    }
+
+    @Override
+    public int hashCode() {
+        int hash = 7;
+        hash = 61 * hash + this.major;
+        hash = 61 * hash + this.minor;
+        hash = 61 * hash + this.patch;
+        return hash;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+
+        if (obj == null || !(obj instanceof GuacamoleProtocolVersion))
+            return false;
+
+        // Versions are equal if all major/minor/patch components are identical
+        final GuacamoleProtocolVersion otherVersion = (GuacamoleProtocolVersion) obj;
+        return this.major == otherVersion.getMajor()
+            && this.minor == otherVersion.getMinor()
+            && this.patch == otherVersion.getPatch();
+
+    }
+
+    @Override
+    public String toString() {
+        return "VERSION_" + getMajor() + "_" + getMinor() + "_" + getPatch();
+    }
+    
+}
diff --git a/guacamole-common/src/test/java/org/apache/guacamole/protocol/GuacamoleProtocolVersionTest.java b/guacamole-common/src/test/java/org/apache/guacamole/protocol/GuacamoleProtocolVersionTest.java
new file mode 100644
index 0000000..d5082b5
--- /dev/null
+++ b/guacamole-common/src/test/java/org/apache/guacamole/protocol/GuacamoleProtocolVersionTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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.protocol;
+
+import org.junit.Assert;
+import org.junit.Test;
+
+/**
+ * Unit test for GuacamoleProtocolVersion. Verifies that Guacamole protocol
+ * version string parsing works as required.
+ */
+public class GuacamoleProtocolVersionTest {
+
+    /**
+     * Verifies that valid version strings are parsed successfully.
+     */
+    @Test
+    public void testValidVersionParse() {
+        GuacamoleProtocolVersion version = GuacamoleProtocolVersion.parseVersion("VERSION_012_102_398");
+        Assert.assertNotNull(version);
+        Assert.assertEquals(12, version.getMajor());
+        Assert.assertEquals(102, version.getMinor());
+        Assert.assertEquals(398, version.getPatch());
+    }
+
+    /**
+     * Verifies that invalid version strings fail to parse.
+     */
+    @Test
+    public void testInvalidVersionParse() {
+
+        Assert.assertNull(GuacamoleProtocolVersion.parseVersion("potato"));
+        Assert.assertNull(GuacamoleProtocolVersion.parseVersion("VERSION_"));
+        Assert.assertNull(GuacamoleProtocolVersion.parseVersion("VERSION___"));
+
+        Assert.assertNull(GuacamoleProtocolVersion.parseVersion("VERSION__2_3"));
+        Assert.assertNull(GuacamoleProtocolVersion.parseVersion("VERSION_1__3"));
+        Assert.assertNull(GuacamoleProtocolVersion.parseVersion("VERSION_1_2_"));
+
+        Assert.assertNull(GuacamoleProtocolVersion.parseVersion("VERSION_A_2_3"));
+        Assert.assertNull(GuacamoleProtocolVersion.parseVersion("VERSION_1_B_3"));
+        Assert.assertNull(GuacamoleProtocolVersion.parseVersion("VERSION_1_2_C"));
+
+        Assert.assertNull(GuacamoleProtocolVersion.parseVersion("_1_2_3"));
+        Assert.assertNull(GuacamoleProtocolVersion.parseVersion("version_1_2_3"));
+
+    }
+
+    /**
+     * Verifies that the atLeast() function defined by GuacamoleProtocolVersion
+     * behaves as required for a series of three versions which are in strictly
+     * increasing order (a &lt; b &lt; c).
+     *
+     * @param a
+     *     The String representation of the version which is known to be the
+     *     smaller than versions b and c.
+     *
+     * @param b
+     *     The String representation of the version which is known to be
+     *     larger than version a but smaller than version c.
+     *
+     * @param c
+     *     The String representation of the version which is known to be the
+     *     larger than versions a and b.
+     */
+    private void testVersionCompare(String a, String b, String c) {
+
+        GuacamoleProtocolVersion verA = GuacamoleProtocolVersion.parseVersion(a);
+        GuacamoleProtocolVersion verB = GuacamoleProtocolVersion.parseVersion(b);
+        GuacamoleProtocolVersion verC = GuacamoleProtocolVersion.parseVersion(c);
+
+        Assert.assertTrue(verC.atLeast(verB));
+        Assert.assertTrue(verC.atLeast(verA));
+        Assert.assertTrue(verB.atLeast(verA));
+
+        Assert.assertFalse(verB.atLeast(verC));
+        Assert.assertFalse(verA.atLeast(verC));
+        Assert.assertFalse(verA.atLeast(verB));
+
+        Assert.assertTrue(verA.atLeast(verA));
+        Assert.assertTrue(verB.atLeast(verB));
+        Assert.assertTrue(verC.atLeast(verC));
+
+    }
+
+    /**
+     * Verifies that version order comparisons using atLeast() behave as
+     * required.
+     */
+    @Test
+    public void testVersionCompare() {
+        testVersionCompare("VERSION_0_0_1", "VERSION_0_0_2", "VERSION_0_0_3");
+        testVersionCompare("VERSION_0_1_0", "VERSION_0_2_0", "VERSION_0_3_0");
+        testVersionCompare("VERSION_1_0_0", "VERSION_2_0_0", "VERSION_3_0_0");
+        testVersionCompare("VERSION_1_2_3", "VERSION_1_3_3", "VERSION_2_0_0");
+    }
+
+    /**
+     * Verifies that versions can be tested for equality using equals().
+     */
+    @Test
+    public void testVersionEquals() {
+
+        GuacamoleProtocolVersion version;
+
+        version = GuacamoleProtocolVersion.parseVersion("VERSION_012_102_398");
+        Assert.assertTrue(version.equals(version));
+        Assert.assertTrue(version.equals(new GuacamoleProtocolVersion(12, 102, 398)));
+        Assert.assertFalse(version.equals(new GuacamoleProtocolVersion(12, 102, 399)));
+        Assert.assertFalse(version.equals(new GuacamoleProtocolVersion(12, 103, 398)));
+        Assert.assertFalse(version.equals(new GuacamoleProtocolVersion(11, 102, 398)));
+
+        version = GuacamoleProtocolVersion.parseVersion("VERSION_1_0_0");
+        Assert.assertTrue(version.equals(GuacamoleProtocolVersion.VERSION_1_0_0));
+        Assert.assertFalse(version.equals(GuacamoleProtocolVersion.VERSION_1_1_0));
+
+        version = GuacamoleProtocolVersion.parseVersion("VERSION_1_1_0");
+        Assert.assertTrue(version.equals(GuacamoleProtocolVersion.VERSION_1_1_0));
+        Assert.assertFalse(version.equals(GuacamoleProtocolVersion.VERSION_1_0_0));
+
+    }
+
+    /**
+     * Verifies that versions can be converted to their Guacamole protocol
+     * representation through calling toString().
+     */
+    @Test
+    public void testToString() {
+        Assert.assertEquals("VERSION_1_0_0", GuacamoleProtocolVersion.VERSION_1_0_0.toString());
+        Assert.assertEquals("VERSION_1_1_0", GuacamoleProtocolVersion.VERSION_1_1_0.toString());
+        Assert.assertEquals("VERSION_12_103_398", new GuacamoleProtocolVersion(12, 103, 398).toString());
+    }
+
+}
diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh
index 6d5af95..be97901 100755
--- a/guacamole-docker/bin/start.sh
+++ b/guacamole-docker/bin/start.sh
@@ -575,6 +575,7 @@
 start_guacamole() {
 
     # Install webapp
+    rm -Rf /usr/local/tomcat/webapps/${WEBAPP_CONTEXT:-guacamole}
     ln -sf /opt/guacamole/guacamole.war /usr/local/tomcat/webapps/${WEBAPP_CONTEXT:-guacamole}.war
 
     # Start tomcat
diff --git a/guacamole-ext/pom.xml b/guacamole-ext/pom.xml
index e123b9f..a4e7b07 100644
--- a/guacamole-ext/pom.xml
+++ b/guacamole-ext/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-ext</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-ext</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -155,7 +155,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>compile</scope>
         </dependency>
 
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 19f1ead..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
@@ -92,6 +92,14 @@
         public static String TIMEZONE = "TIMEZONE";
 
         /**
+         * Field type which allows selection of languages. The languages
+         * displayed are the set of languages supported by the Guacamole web
+         * application. Legal values are valid language IDs, as dictated by
+         * the filenames of Guacamole's available translations.
+         */
+        public static String LANGUAGE = "LANGUAGE";
+
+        /**
          * A date field whose legal values conform to the pattern "YYYY-MM-DD",
          * zero-padded.
          */
@@ -109,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/LanguageField.java b/guacamole-ext/src/main/java/org/apache/guacamole/form/LanguageField.java
new file mode 100644
index 0000000..a87d772
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/form/LanguageField.java
@@ -0,0 +1,64 @@
+/*
+ * 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 language field. The field may contain only valid language
+ * identifiers as used by the Guacamole web application for its translations.
+ * Language identifiers are defined by the filenames of the JSON files
+ * containing the translation.
+ */
+public class LanguageField extends Field {
+
+    /**
+     * Creates a new LanguageField with the given name.
+     *
+     * @param name
+     *     The unique name to associate with this field.
+     */
+    public LanguageField(String name) {
+        super(name, Field.Type.LANGUAGE);
+    }
+
+    /**
+     * Parses the given string into a language ID string. As any string may be
+     * a valid language ID as long as it has a corresponding translation, the
+     * only transformation currently performed by this function is to ensure
+     * that a blank language string is parsed into null.
+     *
+     * @param language
+     *     The language string to parse, which may be null.
+     *
+     * @return
+     *     The ID of the language corresponding to the given string, or null if
+     *     if the given language string was null or blank.
+     */
+    public static String parse(String language) {
+
+        // Return null if no language is provided
+        if (language == null || language.isEmpty())
+            return null;
+
+        // Otherwise, assume language is already a valid language ID
+        return language;
+
+    }
+
+}
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/form/TimeZoneField.java b/guacamole-ext/src/main/java/org/apache/guacamole/form/TimeZoneField.java
index 9305410..90084e4 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/form/TimeZoneField.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/form/TimeZoneField.java
@@ -20,8 +20,9 @@
 package org.apache.guacamole.form;
 
 /**
- * Represents a time zone field. The field may contain only valid time zone IDs,
- * as dictated by TimeZone.getAvailableIDs().
+ * Represents a time zone field. The field may contain only valid time zone
+ * identifiers, as defined by the IANA time zone database. Such identifiers are
+ * also valid Java time zone IDs as dictated by TimeZone.getAvailableIDs().
  */
 public class TimeZoneField extends Field {
 
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/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/TokenName.java b/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenName.java
similarity index 60%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/TokenName.java
rename to guacamole-ext/src/main/java/org/apache/guacamole/token/TokenName.java
index 90de5bf..ae83346 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/TokenName.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenName.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.ldap;
+package org.apache.guacamole.token;
 
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -28,19 +28,13 @@
 public class TokenName {
 
     /**
-     * The prefix string to add to each parameter token generated from an LDAP
-     * attribute name.
-     */
-    private static final String LDAP_ATTRIBUTE_TOKEN_PREFIX = "LDAP_";
-
-    /**
-     * Pattern which matches logical groupings of words within an LDAP
-     * attribute name. This pattern is intended to match logical groupings
+     * Pattern which matches logical groupings of words within a
+     * string. This pattern is intended to match logical groupings
      * regardless of the naming convention used: "CamelCase",
      * "headlessCamelCase", "lowercase_with_underscores",
      * "lowercase-with-dashes" or even "aVery-INCONSISTENTMix_ofAllStyles".
      */
-    private static final Pattern LDAP_ATTRIBUTE_NAME_GROUPING = Pattern.compile(
+    private static final Pattern STRING_NAME_GROUPING = Pattern.compile(
 
         // "Camel" word groups
         "\\p{javaUpperCase}\\p{javaLowerCase}+"
@@ -67,31 +61,35 @@
 
     /**
      * Generates the name of the parameter token that should be populated with
-     * the value of the given LDAP attribute. The name of the LDAP attribute
-     * will automatically be transformed from "CamelCase", "headlessCamelCase",
-     * "lowercase_with_underscores", and "mixes_ofBoth_Styles" to consistent
-     * "UPPERCASE_WITH_UNDERSCORES". Each returned attribute will be prefixed
-     * with "LDAP_".
+     * the given string. The provided string will be automatically transformed
+     * from "CamelCase", "headlessCamelCase", "lowercase_with_underscores",
+     * and "mixes_ofBoth_Styles" to consistent "UPPERCASE_WITH_UNDERSCORES".
+     * Each returned token name will be prefixed with the string value provided
+     * in the prefix.  The value provided in prefix will be prepended to the
+     * string, but will itself not be transformed.
      *
      * @param name
-     *     The name of the LDAP attribute to use to generate the token name.
+     *     The string to be used to generate the token name.
+     * 
+     * @param prefix
+     *     The prefix to prepend to the generated token name.
      *
      * @return
      *     The name of the parameter token that should be populated with the
-     *     value of the LDAP attribute having the given name.
+     *     given string.
      */
-    public static String fromAttribute(String name) {
+    public static String canonicalize(final String name, final String prefix) {
 
         // If even one logical word grouping cannot be found, default to
-        // simply converting the attribute to uppercase and adding the
+        // simply converting the string to uppercase and adding the
         // prefix
-        Matcher groupMatcher = LDAP_ATTRIBUTE_NAME_GROUPING.matcher(name);
+        Matcher groupMatcher = STRING_NAME_GROUPING.matcher(name);
         if (!groupMatcher.find())
-            return LDAP_ATTRIBUTE_TOKEN_PREFIX + name.toUpperCase();
+            return prefix + name.toUpperCase();
 
         // Split the given name into logical word groups, separated by
         // underscores and converted to uppercase
-        StringBuilder builder = new StringBuilder(LDAP_ATTRIBUTE_TOKEN_PREFIX);
+        StringBuilder builder = new StringBuilder(prefix);
         builder.append(groupMatcher.group(0).toUpperCase());
 
         while (groupMatcher.find()) {
@@ -102,5 +100,23 @@
         return builder.toString();
 
     }
+    
+    /**
+     * Generate the name of a parameter token from the given string, with no
+     * added prefix, such that the token name will simply be the transformed
+     * version of the string. See
+     * {@link #canonicalize(java.lang.String, java.lang.String)}
+     * 
+     * 
+     * @param name
+     *     The string to use to generate the token name.
+     * 
+     * @return 
+     *     The name of the parameter token that should be populated with the
+     *     given string.
+     */
+    public static String canonicalize(final String name) {
+        return canonicalize(name, "");
+    }
 
 }
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/rdp.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json
index e3de15f..3c9d4fe 100644
--- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/rdp.json
@@ -91,6 +91,7 @@
                     "type"    : "ENUM",
                     "options" : [
                         "",
+                        "de-ch-qwertz",
                         "de-de-qwertz",
                         "en-gb-qwerty",
                         "en-us-qwerty",
@@ -107,6 +108,10 @@
                     ]
                 },
                 {
+                    "name"  : "timezone",
+                    "type"  : "TIMEZONE"
+                },
+                {
                     "name"    : "console",
                     "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 a71e1fb..6a91279 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" ]
                 },
                 {
@@ -60,6 +60,10 @@
                     "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" ]
@@ -75,6 +79,14 @@
                     "type"  : "TEXT"
                 },
                 {
+                    "name"  : "locale",
+                    "type"  : "TEXT"
+                },
+                {
+                    "name"  : "timezone",
+                    "type"  : "TIMEZONE"
+                },
+                {
                     "name"  : "server-alive-interval",
                     "type"  : "NUMERIC"
                 }
@@ -89,7 +101,7 @@
                     "options" : [ "", "127", "8" ]
                 },
                 {
-                    "name"  : "terminal-type",
+                    "name"    : "terminal-type",
                     "type"    : "ENUM",
                     "options" : [ "", "xterm", "xterm-256color", "vt220", "vt100", "ansi", "linux" ]
                 }
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 42d5be0..2526096 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" ]
                 },
                 {
@@ -64,6 +64,10 @@
                     "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" ]
diff --git a/guacamole-ext/src/test/java/org/apache/guacamole/token/TokenNameTest.java b/guacamole-ext/src/test/java/org/apache/guacamole/token/TokenNameTest.java
new file mode 100644
index 0000000..a1d3e2f
--- /dev/null
+++ b/guacamole-ext/src/test/java/org/apache/guacamole/token/TokenNameTest.java
@@ -0,0 +1,55 @@
+/*
+ * 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.token;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+/**
+ * Test which verifies automatic generation of connection parameter token names.
+ */
+public class TokenNameTest {
+
+    /**
+     * Verifies that TokenName.canonicalize() generates token names as
+     * specified, regardless of the format of the provided string.
+     */
+    @Test
+    public void testCanonicalize() {
+        assertEquals("A", TokenName.canonicalize("a"));
+        assertEquals("B", TokenName.canonicalize("b"));
+        assertEquals("1", TokenName.canonicalize("1"));
+        assertEquals("SOME_URL", TokenName.canonicalize("someURL"));
+        assertEquals("LOWERCASE_WITH_DASHES", TokenName.canonicalize("lowercase-with-dashes"));
+        assertEquals("HEADLESS_CAMEL_CASE", TokenName.canonicalize("headlessCamelCase"));
+        assertEquals("CAMEL_CASE", TokenName.canonicalize("CamelCase"));
+        assertEquals("CAMEL_CASE", TokenName.canonicalize("CamelCase"));
+        assertEquals("LOWERCASE_WITH_UNDERSCORES", TokenName.canonicalize("lowercase_with_underscores"));
+        assertEquals("UPPERCASE_WITH_UNDERSCORES", TokenName.canonicalize("UPPERCASE_WITH_UNDERSCORES"));
+        assertEquals("A_VERY_INCONSISTENT_MIX_OF_ALL_STYLES", TokenName.canonicalize("aVery-INCONSISTENTMix_ofAllStyles"));
+        assertEquals("ABC_123_DEF_456", TokenName.canonicalize("abc123def456"));
+        assertEquals("ABC_123_DEF_456", TokenName.canonicalize("ABC123DEF456"));
+        assertEquals("WORD_A_WORD_AB_WORD_ABC_WORD", TokenName.canonicalize("WordAWordABWordABCWord"));
+        
+        assertEquals("AUTH_ATTRIBUTE", TokenName.canonicalize("Attribute", "AUTH_"));
+        assertEquals("auth_SOMETHING", TokenName.canonicalize("Something", "auth_"));
+    }
+
+}
diff --git a/guacamole/pom.xml b/guacamole/pom.xml
index e1a2b47..8f90f27 100644
--- a/guacamole/pom.xml
+++ b/guacamole/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole</artifactId>
     <packaging>war</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -264,21 +264,21 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
         <!-- Guacamole Extension API -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
         <!-- Guacamole JavaScript API -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common-js</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <type>zip</type>
             <scope>runtime</scope>
         </dependency>
@@ -493,6 +493,20 @@
             </exclusions>
 
         </dependency>
+        
+        <!-- JSTZ for TimeZone Detection -->
+        <dependency>
+            <groupId>org.webjars.npm</groupId>
+            <artifactId>jstz</artifactId>
+            <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>
 
diff --git a/guacamole/src/licenses/LICENSE b/guacamole/src/licenses/LICENSE
index 51f5b21..7c696e5 100644
--- a/guacamole/src/licenses/LICENSE
+++ b/guacamole/src/licenses/LICENSE
@@ -605,6 +605,36 @@
 terms above.
 
 
+JSTZ (https://pellepim.bitbucket.io/jstz/)
+------------------------------------------
+
+    Version: 1.0.10
+    From: 'Jon Nylander' (https://pellepim.bitbucket.io/jstz/)
+    License(s):
+        MIT (bundled/jstz-1.0.10/LICENSE)
+
+Copyright (c) 2012 Jon Nylander, project maintained at 
+https://bitbucket.org/pellepim/jstimezonedetect
+
+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.
+
+
 Logback (http://logback.qos.ch/)
 --------------------------------
 
@@ -658,6 +688,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/jstz-1.0.10/LICENSE b/guacamole/src/licenses/bundled/jstz-1.0.10/LICENSE
new file mode 100644
index 0000000..c48af16
--- /dev/null
+++ b/guacamole/src/licenses/bundled/jstz-1.0.10/LICENSE
@@ -0,0 +1,22 @@
+MIT License 
+
+Copyright (c) 2012 Jon Nylander, project maintained at 
+https://bitbucket.org/pellepim/jstimezonedetect
+
+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.
\ No newline at end of file
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/extension/ExtensionModule.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
index ae8c463..0a424ab 100644
--- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
+++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
@@ -62,7 +62,8 @@
     private static final List<String> ALLOWED_GUACAMOLE_VERSIONS =
         Collections.unmodifiableList(Arrays.asList(
             "*",
-            "1.0.0"
+            "1.0.0",
+            "1.1.0"
         ));
 
     /**
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/tunnel/TunnelRequest.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequest.java
index 8c23dab..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;
 
 /**
@@ -95,40 +93,11 @@
      * once for each mimetype.
      */
     public static final String IMAGE_PARAMETER = "GUAC_IMAGE";
-
+    
     /**
-     * All supported object types that can be used as the destination of a
-     * tunnel.
+     * The name of the parameter specifying the timezone of the client.
      */
-    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;
-        }
-
-    };
+    public static final String TIMEZONE_PARAMETER = "GUAC_TIMEZONE";
 
     /**
      * Returns the value of the parameter having the given name.
@@ -252,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.");
 
@@ -365,5 +327,16 @@
     public List<String> getImageMimetypes() {
         return getParameterValues(IMAGE_PARAMETER);
     }
-
+    
+    /**
+     * Returns the tz database value of the timezone declared by the client
+     * within the tunnel request.
+     * 
+     * @return 
+     *     The tz database value of the timezone parameter as reported by
+     *     the client.
+     */
+    public String getTimezone() {
+        return getParameter(TIMEZONE_PARAMETER);
+    }
 }
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 1479d82..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;
@@ -166,6 +164,11 @@
         List<String> imageMimetypes = request.getImageMimetypes();
         if (imageMimetypes != null)
             info.getImageMimetypes().addAll(imageMimetypes);
+        
+        // Set timezone if provided
+        String timezone = request.getTimezone();
+        if (timezone != null && !timezone.isEmpty())
+            info.setTimezone(timezone);
 
         return info;
     }
@@ -199,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;
 
     }
@@ -292,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
@@ -315,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 {
 
@@ -385,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 41c6ba6..b90f263 100644
--- a/guacamole/src/main/webapp/app/client/controllers/clientController.js
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -27,6 +27,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
@@ -248,7 +249,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 : {}
 
     };
 
@@ -258,6 +267,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
@@ -429,12 +448,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;
 
@@ -806,6 +833,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 ad85f23..7690e75 100644
--- a/guacamole/src/main/webapp/app/client/templates/client.html
+++ b/guacamole/src/main/webapp/app/client/templates/client.html
@@ -96,6 +96,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 a9bc3be..800eaf4 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,19 +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 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
@@ -117,6 +121,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.
@@ -178,6 +199,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 || {};
+
     };
 
     /**
@@ -225,6 +257,7 @@
             + "&GUAC_WIDTH="       + Math.floor(optimal_width)
             + "&GUAC_HEIGHT="      + Math.floor(optimal_height)
             + "&GUAC_DPI="         + Math.floor(optimal_dpi)
+            + "&GUAC_TIMEZONE="    + encodeURIComponent(preferenceService.preferences.timezone)
             + (connectionParameters ? '&' + connectionParameters : '');
 
         // Add audio mimetypes to connect string
@@ -446,6 +479,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) {
 
@@ -520,11 +580,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);
         }
         
@@ -536,6 +601,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;
 
     };
@@ -618,6 +706,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/languageFieldController.js b/guacamole/src/main/webapp/app/form/controllers/languageFieldController.js
new file mode 100644
index 0000000..fdab137
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/controllers/languageFieldController.js
@@ -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.
+ */
+
+
+/**
+ * Controller for the language field type. The language field type allows the
+ * user to select a language from the set of languages supported by the
+ * Guacamole web application.
+ */
+angular.module('form').controller('languageFieldController', ['$scope', '$injector',
+    function languageFieldController($scope, $injector) {
+
+    // Required services
+    var languageService = $injector.get('languageService');
+    var requestService  = $injector.get('requestService');
+
+    /**
+     * A map of all available language keys to their human-readable
+     * names.
+     *
+     * @type Object.<String, String>
+     */
+    $scope.languages = null;
+
+    // Retrieve defined languages
+    languageService.getLanguages().then(function languagesRetrieved(languages) {
+        $scope.$apply(function updateLanguageOptions() {
+            $scope.languages = languages;
+        });
+    }, requestService.DIE);
+
+    // Interpret undefined/null as empty string
+    $scope.$watch('model', function setModel(model) {
+        if (!model && model !== '')
+            $scope.model = '';
+    });
+
+}]);
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/controllers/timeZoneFieldController.js b/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js
index 5f17915..39f0c38 100644
--- a/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js
+++ b/guacamole/src/main/webapp/app/form/controllers/timeZoneFieldController.js
@@ -19,8 +19,9 @@
 
 
 /**
- * Controller for time zone fields. Time zone fields use Java IDs as the
- * standard representation for each supported time zone.
+ * Controller for time zone fields. Time zone fields use IANA time zone
+ * database identifiers as the standard representation for each supported time
+ * zone. These identifiers are also legal Java time zone IDs.
  */
 angular.module('form').controller('timeZoneFieldController', ['$scope', '$injector',
     function timeZoneFieldController($scope, $injector) {
@@ -418,7 +419,6 @@
         "Canada" : {
             "Atlantic"          : "Canada/Atlantic",
             "Central"           : "Canada/Central",
-            "East-Saskatchewan" : "Canada/East-Saskatchewan",
             "Eastern"           : "Canada/Eastern",
             "Mountain"          : "Canada/Mountain",
             "Newfoundland"      : "Canada/Newfoundland",
diff --git a/guacamole/src/main/webapp/app/form/directives/formField.js b/guacamole/src/main/webapp/app/form/directives/formField.js
index ea0f35f..41db0c1 100644
--- a/guacamole/src/main/webapp/app/form/directives/formField.js
+++ b/guacamole/src/main/webapp/app/form/directives/formField.js
@@ -73,6 +73,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:
              *
@@ -158,4 +170,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/formModule.js b/guacamole/src/main/webapp/app/form/formModule.js
index 7e6ede9..1135118 100644
--- a/guacamole/src/main/webapp/app/form/formModule.js
+++ b/guacamole/src/main/webapp/app/form/formModule.js
@@ -20,4 +20,7 @@
 /**
  * Module for displaying dynamic forms.
  */
-angular.module('form', ['locale']);
+angular.module('form', [
+    'locale',
+    'rest'
+]);
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 168a1ef..4198c10 100644
--- a/guacamole/src/main/webapp/app/form/services/formService.js
+++ b/guacamole/src/main/webapp/app/form/services/formService.js
@@ -131,6 +131,21 @@
         },
 
         /**
+         * Field type which allows selection of languages. The languages
+         * displayed are the set of languages supported by the Guacamole web
+         * application. Legal values are valid language IDs, as dictated by
+         * the filenames of Guacamole's available translations.
+         *
+         * @see {@link Field.Type.LANGUAGE}
+         * @type FieldType
+         */
+        'LANGUAGE' : {
+            module      : 'form',
+            controller  : 'languageFieldController',
+            templateUrl : 'app/form/templates/languageField.html'
+        },
+
+        /**
          * Field type which allows selection of time zones.
          *
          * @see {@link Field.Type.TIMEZONE}
@@ -164,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'
         }
 
     };
@@ -192,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
@@ -206,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.
@@ -267,7 +335,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 ad9d8e0..110ab14 100644
--- a/guacamole/src/main/webapp/app/form/templates/checkboxField.html
+++ b/guacamole/src/main/webapp/app/form/templates/checkboxField.html
@@ -1 +1,5 @@
-<input type="checkbox" ng-model="typedValue" autocorrect="off" autocapitalize="off"/>
\ No newline at end of file
+<input type="checkbox"
+       ng-attr-id="{{ fieldId }}"
+       ng-model="typedValue"
+       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 a186e19..2da96a1 100644
--- a/guacamole/src/main/webapp/app/form/templates/dateField.html
+++ b/guacamole/src/main/webapp/app/form/templates/dateField.html
@@ -1,5 +1,6 @@
 <div class="date-field">
     <input type="date"
+           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 db6d3be..4ef14f4 100644
--- a/guacamole/src/main/webapp/app/form/templates/emailField.html
+++ b/guacamole/src/main/webapp/app/form/templates/emailField.html
@@ -1,8 +1,9 @@
 <div class="email-field">
     <input type="email"
+           ng-attr-id="{{ fieldId }}"
            ng-model="model"
            ng-hide="readOnly"
            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
new file mode 100644
index 0000000..b1f25e8
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/templates/languageField.html
@@ -0,0 +1,3 @@
+<select 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 3d6312e..f802b4a 100644
--- a/guacamole/src/main/webapp/app/form/templates/numberField.html
+++ b/guacamole/src/main/webapp/app/form/templates/numberField.html
@@ -1 +1,5 @@
-<input type="number" ng-model="typedValue" autocorrect="off" autocapitalize="off"/>
\ No newline at end of file
+<input type="number"
+       ng-attr-id="{{ fieldId }}"
+       ng-model="typedValue"
+       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 506d8b6..69d67e8 100644
--- a/guacamole/src/main/webapp/app/form/templates/passwordField.html
+++ b/guacamole/src/main/webapp/app/form/templates/passwordField.html
@@ -1,4 +1,9 @@
 <div class="password-field">
-    <input type="{{passwordInputType}}" ng-model="model" ng-trim="false" autocorrect="off" autocapitalize="off"/>
+    <input type="{{passwordInputType}}"
+           ng-attr-id="{{ fieldId }}"
+           ng-model="model"
+           ng-trim="false"
+           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 3bd2bb8..e5ce0e9 100644
--- a/guacamole/src/main/webapp/app/form/templates/selectField.html
+++ b/guacamole/src/main/webapp/app/form/templates/selectField.html
@@ -1 +1,3 @@
-<select ng-model="model" ng-options="option as getFieldOption(option) | translate for option in field.options | orderBy: value"></select>
\ No newline at end of file
+<select ng-attr-id="{{ fieldId }}"
+        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 082476f..2d4144b 100644
--- a/guacamole/src/main/webapp/app/form/templates/textAreaField.html
+++ b/guacamole/src/main/webapp/app/form/templates/textAreaField.html
@@ -1 +1,4 @@
-<textarea ng-model="model" autocorrect="off" autocapitalize="off"></textarea>
\ No newline at end of file
+<textarea ng-attr-id="{{ fieldId }}"
+          ng-model="model"
+          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 a338db4..938b81c 100644
--- a/guacamole/src/main/webapp/app/form/templates/textField.html
+++ b/guacamole/src/main/webapp/app/form/templates/textField.html
@@ -1,7 +1,12 @@
 <div class="text-field">
-    <input type="text" ng-model="model" autocorrect="off" autocapitalize="off" ng-attr-list="{{ dataListId }}"/>
-    <datalist ng-if="dataListId" id="{{ dataListId }}">
+    <input type="text"
+           ng-attr-id="{{ fieldId }}"
+           ng-attr-list="{{ dataListId }}"
+           ng-model="model"
+           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 24ae968..292e44c 100644
--- a/guacamole/src/main/webapp/app/form/templates/timeField.html
+++ b/guacamole/src/main/webapp/app/form/templates/timeField.html
@@ -1,5 +1,6 @@
 <div class="time-field">
     <input type="time"
+           ng-attr-id="{{ fieldId }}"
            ng-model="typedValue"
            ng-model-options="modelOptions"
            guac-lenient-time
diff --git a/guacamole/src/main/webapp/app/form/templates/timeZoneField.html b/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
index 15fd4d6..ed960af 100644
--- a/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
+++ b/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
@@ -2,6 +2,7 @@
 
     <!-- Available time zone regions -->
     <select class="time-zone-region"
+            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/guacSettingsPreferences.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js
index 71e7af7..aad0a2e 100644
--- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js
+++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js
@@ -39,7 +39,6 @@
             var $translate            = $injector.get('$translate');
             var authenticationService = $injector.get('authenticationService');
             var guacNotification      = $injector.get('guacNotification');
-            var languageService       = $injector.get('languageService');
             var permissionService     = $injector.get('permissionService');
             var preferenceService     = $injector.get('preferenceService');
             var requestService        = $injector.get('requestService');
@@ -78,21 +77,23 @@
              * @type Object.<String, Object>
              */
             $scope.preferences = preferenceService.preferences;
-            
+
             /**
-             * A map of all available language keys to their human-readable
-             * names.
-             * 
-             * @type Object.<String, String>
+             * The fields which should be displayed for choosing locale
+             * preferences. Each field name must be a property on
+             * $scope.preferences.
+             *
+             * @type Field[]
              */
-            $scope.languages = null;
-            
-            /**
-             * Switches the active display langugae to the chosen language.
-             */
-            $scope.changeLanguage = function changeLanguage() {
-                $translate.use($scope.preferences.language);
-            };
+            $scope.localeFields = [
+                { 'type' : 'LANGUAGE', 'name' : 'language' },
+                { 'type' : 'TIMEZONE', 'name' : 'timezone' }
+            ];
+
+            // Automatically update applied translation when language preference is changed
+            $scope.$watch('preferences.language', function changeLanguage(language) {
+                $translate.use(language);
+            });
 
             /**
              * The new password for the user.
@@ -169,17 +170,6 @@
                 
             };
 
-            // Retrieve defined languages
-            languageService.getLanguages()
-            .then(function languagesRetrieved(languages) {
-                $scope.languages = Object.keys(languages).map(function(key) {
-                    return {
-                        key: key,
-                        value: languages[key]
-                    };
-                });
-            }, requestService.DIE);
-
             // Retrieve current permissions
             permissionService.getEffectivePermissions(dataSource, username)
             .then(function permissionsRetrieved(permissions) {
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/services/preferenceService.js b/guacamole/src/main/webapp/app/settings/services/preferenceService.js
index bcd8633..9c4d5fc 100644
--- a/guacamole/src/main/webapp/app/settings/services/preferenceService.js
+++ b/guacamole/src/main/webapp/app/settings/services/preferenceService.js
@@ -98,6 +98,18 @@
         return language.replace(/-/g, '_');
 
     };
+    
+    /**
+     * Return the timezone detected for the current browser session
+     * by the JSTZ timezone library.
+     * 
+     * @returns String
+     *     The name of the currently-detected timezone in IANA zone key
+     *     format (Olson time zone database).
+     */
+    var getDetectedTimezone = function getDetectedTimezone() {
+        return jstz.determine().name();
+    };
 
     /**
      * All currently-set preferences, as name/value pairs. Each property name
@@ -128,7 +140,15 @@
          * 
          * @type String
          */
-        language : getDefaultLanguageKey()
+        language : getDefaultLanguageKey(),
+        
+        /**
+         * The timezone set by the user, in IANA zone key format (Olson time
+         * zone database).
+         * 
+         * @type String
+         */
+        timezone : getDetectedTimezone()
 
     };
 
diff --git a/guacamole/src/main/webapp/app/settings/styles/preferences.css b/guacamole/src/main/webapp/app/settings/styles/preferences.css
index ed8460d..dbb2330 100644
--- a/guacamole/src/main/webapp/app/settings/styles/preferences.css
+++ b/guacamole/src/main/webapp/app/settings/styles/preferences.css
@@ -17,8 +17,23 @@
  * under the License.
  */
 
-.preferences .update-password .form, 
-.preferences .language .form {
+.preferences .form .fields {
+    display: table;
     padding-left: 0.5em;
-    border-left: 3px solid rgba(0, 0, 0, 0.125);
-}
\ No newline at end of file
+    border-left: 3px solid rgba(0,0,0,0.125);
+}
+
+.preferences .form .fields .labeled-field {
+    display: table-row;
+}
+
+.preferences .form .fields .field-header,
+.preferences .form .fields .form-field {
+    display: table-cell;
+    padding: 0.125em;
+    vertical-align: top;
+}
+
+.preferences .form .fields .field-header {
+    padding-right: 1em;
+}
diff --git a/guacamole/src/main/webapp/app/settings/templates/settingsPreferences.html b/guacamole/src/main/webapp/app/settings/templates/settingsPreferences.html
index 826a5cd..581a66e 100644
--- a/guacamole/src/main/webapp/app/settings/templates/settingsPreferences.html
+++ b/guacamole/src/main/webapp/app/settings/templates/settingsPreferences.html
@@ -1,18 +1,9 @@
 <div class="preferences" ng-class="{loading: !isLoaded()}">
 
-    <!-- Language settings -->
-    <div class="settings section language">
-        <p>{{'SETTINGS_PREFERENCES.HELP_LANGUAGE' | translate}}</p>
-
-        <!-- Language selection -->
-        <div class="form">
-            <table class="fields">
-                <tr>
-                    <th>{{'SETTINGS_PREFERENCES.FIELD_HEADER_LANGUAGE' | translate}}</th>
-                    <td><select ng-model="preferences.language" ng-change="changeLanguage()" ng-options="language.key as language.value for language in languages | orderBy: key"></select></td>
-                </tr>
-            </table>
-        </div>
+    <!-- Locale settings -->
+    <div class="settings section locale">
+        <p>{{'SETTINGS_PREFERENCES.HELP_LOCALE' | translate}}</p>
+        <guac-form content="localeFields" model="preferences" namespace="'SETTINGS_PREFERENCES'"></guac-form>
     </div>
     
     <!-- Password update -->
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 1d51606..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>
@@ -85,6 +86,12 @@
         <script type="text/javascript" src="webjars/angular-translate-interpolation-messageformat/2.16.0/angular-translate-interpolation-messageformat.min.js"></script>
         <script type="text/javascript" src="webjars/angular-translate-loader-static-files/2.16.0/angular-translate-loader-static-files.min.js"></script>
 
+        <!-- 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 9fc69ef..7b29549 100644
--- a/guacamole/src/main/webapp/translations/en.json
+++ b/guacamole/src/main/webapp/translations/en.json
@@ -148,6 +148,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)"
     },
@@ -362,7 +378,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:",
@@ -428,6 +510,7 @@
         "FIELD_HEADER_SFTP_ROOT_DIRECTORY"        : "File browser root directory:",
         "FIELD_HEADER_SFTP_USERNAME"              : "Username:",
         "FIELD_HEADER_STATIC_CHANNELS" : "Static channel names:",
+        "FIELD_HEADER_TIMEZONE"        : "Time zone:",
         "FIELD_HEADER_USERNAME"        : "Username:",
         "FIELD_HEADER_WIDTH"           : "Width:",
 
@@ -447,6 +530,7 @@
         "FIELD_OPTION_SECURITY_RDP"   : "RDP encryption",
         "FIELD_OPTION_SECURITY_TLS"   : "TLS encryption",
 
+        "FIELD_OPTION_SERVER_LAYOUT_DE_CH_QWERTZ" : "Swiss German (Qwertz)",
         "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "German (Qwertz)",
         "FIELD_OPTION_SERVER_LAYOUT_EMPTY"        : "",
         "FIELD_OPTION_SERVER_LAYOUT_EN_GB_QWERTY" : "UK English (Qwerty)",
@@ -491,11 +575,13 @@
         "FIELD_HEADER_ENABLE_SFTP"   : "Enable SFTP:",
         "FIELD_HEADER_HOST_KEY"      : "Public host key (Base64):",
         "FIELD_HEADER_HOSTNAME"      : "Hostname:",
+        "FIELD_HEADER_LOCALE"        : "Language/Locale ($LANG):",
         "FIELD_HEADER_USERNAME"      : "Username:",
         "FIELD_HEADER_PASSWORD"      : "Password:",
         "FIELD_HEADER_PASSPHRASE"    : "Passphrase:",
         "FIELD_HEADER_PORT"          : "Port:",
         "FIELD_HEADER_PRIVATE_KEY"   : "Private key:",
+        "FIELD_HEADER_SCROLLBACK"    : "Maximum scrollback size:",
         "FIELD_HEADER_READ_ONLY"     : "Read-only:",
         "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "Exclude mouse:",
         "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclude graphics/streams:",
@@ -505,6 +591,7 @@
         "FIELD_HEADER_SERVER_ALIVE_INTERVAL" : "Server keepalive interval:",
         "FIELD_HEADER_SFTP_ROOT_DIRECTORY"   : "File browser root directory:",
         "FIELD_HEADER_TERMINAL_TYPE"   : "Terminal type:",
+        "FIELD_HEADER_TIMEZONE"        : "Time zone ($TZ):",
         "FIELD_HEADER_TYPESCRIPT_NAME" : "Typescript name:",
         "FIELD_HEADER_TYPESCRIPT_PATH" : "Typescript path:",
 
@@ -577,6 +664,7 @@
         "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_TERMINAL_TYPE"   : "Terminal type:",
         "FIELD_HEADER_TYPESCRIPT_NAME" : "Typescript name:",
         "FIELD_HEADER_TYPESCRIPT_PATH" : "Typescript path:",
@@ -754,6 +842,7 @@
         "FIELD_HEADER_PASSWORD_OLD"       : "Current Password:",
         "FIELD_HEADER_PASSWORD_NEW"       : "New Password:",
         "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Confirm New Password:",
+        "FIELD_HEADER_TIMEZONE"           : "Timezone:",
         "FIELD_HEADER_USERNAME"           : "Username:",
         
         "HELP_DEFAULT_INPUT_METHOD" : "The default input method determines how keyboard events are received by Guacamole. Changing this setting may be necessary when using a mobile device, or when typing through an IME. This setting can be overridden on a per-connection basis within the Guacamole menu.",
@@ -761,7 +850,7 @@
         "HELP_INPUT_METHOD_NONE"    : "@:CLIENT.HELP_INPUT_METHOD_NONE",
         "HELP_INPUT_METHOD_OSK"     : "@:CLIENT.HELP_INPUT_METHOD_OSK",
         "HELP_INPUT_METHOD_TEXT"    : "@:CLIENT.HELP_INPUT_METHOD_TEXT",
-        "HELP_LANGUAGE"             : "Select a different language below to change the language of all text within Guacamole. Available choices will depend on which languages are installed.",
+        "HELP_LOCALE"               : "Options below are related to the locale of the user and will impact how various parts of the interface are displayed.",
         "HELP_MOUSE_MODE_ABSOLUTE"  : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE",
         "HELP_MOUSE_MODE_RELATIVE"  : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE",
         "HELP_UPDATE_PASSWORD"      : "If you wish to change your password, enter your current password and the desired new password below, and click \"Update Password\". The change will take effect immediately.",
diff --git a/guacamole/src/main/webapp/translations/zh.json b/guacamole/src/main/webapp/translations/zh.json
index eca8656..b940418 100644
--- a/guacamole/src/main/webapp/translations/zh.json
+++ b/guacamole/src/main/webapp/translations/zh.json
@@ -4,9 +4,6 @@
     
     "APP" : {
 
-        "NAME"    : "Apache Guacamole",
-        "VERSION" : "${project.version}",
-
         "ACTION_ACKNOWLEDGE"        : "确定",
         "ACTION_CANCEL"             : "取消",
         "ACTION_CLONE"              : "克隆",
diff --git a/pom.xml b/pom.xml
index 480eb23..83cfa8e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-client</artifactId>
     <packaging>pom</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-client</name>
     <url>http://guacamole.apache.org/</url>