GUACAMOLE-774: Merge dynamic loading of MD4 support (for RADIUS MSCHAPv1/2).

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 958ea2c..fce4760 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
@@ -30,12 +30,15 @@
 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 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;
@@ -53,6 +56,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.
@@ -61,9 +69,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.
@@ -73,13 +81,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,
@@ -89,33 +99,43 @@
         validator.setAcceptAnyProxy(true);
         validator.setEncoding("UTF-8");
         try {
+            Map<String, String> tokens = new HashMap<>();
             URI confRedirectURI = confService.getRedirectURI();
             Assertion a = validator.validate(ticket, confRedirectURI.toString());
             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 d037fbb..40d0334 100644
--- a/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
@@ -10,7 +10,8 @@
     ],
 
     "translations" : [
-        "translations/en.json"
+        "translations/en.json",
+        "translations/ja.json"
     ],
 
     "js" : [
diff --git a/extensions/guacamole-auth-cas/src/main/resources/translations/ja.json b/extensions/guacamole-auth-cas/src/main/resources/translations/ja.json
new file mode 100644
index 0000000..2afdb76
--- /dev/null
+++ b/extensions/guacamole-auth-cas/src/main/resources/translations/ja.json
@@ -0,0 +1,7 @@
+{
+
+    "LOGIN" : {
+        "INFO_CAS_REDIRECT_PENDING"  : "CAS認証にリダイレクトしています。"
+    }
+
+}
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 6de72fa..7daefdd 100644
--- a/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
@@ -10,7 +10,8 @@
     ],
 
     "translations" : [
-        "translations/en.json"
+        "translations/en.json",
+        "translations/ja.json"
     ],
 
     "js" : [
diff --git a/extensions/guacamole-auth-duo/src/main/resources/translations/ja.json b/extensions/guacamole-auth-duo/src/main/resources/translations/ja.json
new file mode 100644
index 0000000..37ddde2
--- /dev/null
+++ b/extensions/guacamole-auth-duo/src/main/resources/translations/ja.json
@@ -0,0 +1,8 @@
+{
+
+    "LOGIN" : {
+        "INFO_DUO_VALIDATION_CODE_INCORRECT"    : "Duoの認証コードが間違っています。",
+        "INFO_DUO_AUTH_REQUIRED"                : "Duoで認証してください。"
+    }
+
+}
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-base/src/main/resources/translations/ja.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/ja.json
new file mode 100644
index 0000000..de5da9a
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/resources/translations/ja.json
@@ -0,0 +1,96 @@
+{
+
+    "LOGIN" : {
+
+        "ERROR_PASSWORD_BLANK"    : "@:APP.ERROR_PASSWORD_BLANK",
+        "ERROR_PASSWORD_SAME"     : "新しいパスワードには、無効になったパスワードとは別の文字を使用してください。",
+        "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
+        "ERROR_NOT_VALID"         : "このユーザアカウントは、現在有効ではありません。",
+        "ERROR_NOT_ACCESSIBLE"    : "このユーザのアクセスは現在許可されていません。 しばらくしてから再度アクセスをしてください。",
+
+        "INFO_PASSWORD_EXPIRED" : "あなたのパスワードは無効なため、リセットが必要です。 新しいパスワードを入力してください。",
+
+        "FIELD_HEADER_NEW_PASSWORD"         : "新しいパスワード",
+        "FIELD_HEADER_CONFIRM_NEW_PASSWORD" : "新しいパスワード(確認)"
+
+    },
+
+    "CONNECTION_ATTRIBUTES" : {
+
+        "FIELD_HEADER_MAX_CONNECTIONS"          : "最大接続数:",
+        "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "ユーザ毎の最大接続数:",
+
+        "FIELD_HEADER_FAILOVER_ONLY"            : "フェイルオーバ機能のみを使用:",
+        "FIELD_HEADER_WEIGHT"                   : "コネクションウェイト:",
+
+        "FIELD_HEADER_GUACD_HOSTNAME"   : "ホスト名:",
+        "FIELD_HEADER_GUACD_ENCRYPTION" : "暗号化:",
+        "FIELD_HEADER_GUACD_PORT"       : "ポート:",
+
+        "FIELD_OPTION_GUACD_ENCRYPTION_NONE"  : "なし (暗号化なし)",
+
+        "SECTION_HEADER_CONCURRENCY"    : "同時接続制限",
+        "SECTION_HEADER_LOAD_BALANCING" : "ロードバラシング",
+        "SECTION_HEADER_GUACD"          : "Guacamoleプロキシパラメータ (guacd)"
+
+    },
+
+    "CONNECTION_GROUP_ATTRIBUTES" : {
+
+        "FIELD_HEADER_ENABLE_SESSION_AFFINITY"  : "セッションアフィニティの有効化:",
+        "FIELD_HEADER_MAX_CONNECTIONS"          : "最大接続数:",
+        "FIELD_HEADER_MAX_CONNECTIONS_PER_USER" : "ユーザ毎の最大接続数:",
+
+        "SECTION_HEADER_CONCURRENCY" : "同時接続制限(バランシンググループ)"
+
+    },
+
+    "DATA_SOURCE_MYSQL_SHARED" : {
+        "NAME" : "共有接続 (MySQL)"
+    },
+
+    "DATA_SOURCE_POSTGRESQL_SHARED" : {
+        "NAME" : "共有接続 (PostgreSQL)"
+    },
+
+    "DATA_SOURCE_SQLSERVER_SHARED" : {
+        "NAME" : "共有接続 (SQL Server)"
+    },
+
+    "HOME" : {
+        "INFO_SHARED_BY" : "{USERNAME}によって共有されています"
+    },
+
+    "PASSWORD_POLICY" : {
+
+        "ERROR_CONTAINS_USERNAME"      : "ユーザ名にパスワードを含んでいます。",
+        "ERROR_REQUIRES_DIGIT"         : "パスワードには数字を含めてください。",
+        "ERROR_REQUIRES_MULTIPLE_CASE" : "パスワードにはアルファベットの大文字・小文字を含めてください。",
+        "ERROR_REQUIRES_NON_ALNUM"     : "パスワードには記号を含めてください。"
+
+    },
+
+    "USER_ATTRIBUTES" : {
+
+        "FIELD_HEADER_DISABLED"            : "ログインの無効化:",
+        "FIELD_HEADER_EXPIRED"             : "パスワードの期限:",
+        "FIELD_HEADER_ACCESS_WINDOW_END"   : "指定した時刻からアクセスを禁止する:",
+        "FIELD_HEADER_ACCESS_WINDOW_START" : "指定した時刻からアクセスを許可する:",
+        "FIELD_HEADER_TIMEZONE"            : "ユーザのタイムゾーン:",
+        "FIELD_HEADER_VALID_FROM"          : "指定した日からアカウントを有効化する:",
+        "FIELD_HEADER_VALID_UNTIL"         : "指定した日からアカウントを無効化する:",
+
+        "SECTION_HEADER_RESTRICTIONS" : "アカウント制限",
+        "SECTION_HEADER_PROFILE"      : "プロフィール"
+
+    },
+
+    "USER_GROUP_ATTRIBUTES" : {
+
+        "FIELD_HEADER_DISABLED" : "グループの無効化:",
+
+        "SECTION_HEADER_RESTRICTIONS" : "グループ制限"
+
+    }
+
+}
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 d80b594..639b7fc 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
@@ -22,6 +22,7 @@
         "translations/en.json",
         "translations/es.json",
         "translations/fr.json",
+        "translations/ja.json",
         "translations/ru.json"
     ]
 
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/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.json
index 6b2d17c..ce59c77 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
@@ -22,6 +22,7 @@
         "translations/en.json",
         "translations/es.json",
         "translations/fr.json",
+        "translations/ja.json",
         "translations/ru.json"
     ]
 
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 79286f1..ba65007 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
@@ -22,6 +22,7 @@
         "translations/en.json",
         "translations/es.json",
         "translations/fr.json",
+        "translations/ja.json",
         "translations/ru.json"
     ]
 
diff --git a/extensions/guacamole-auth-ldap/pom.xml b/extensions/guacamole-auth-ldap/pom.xml
index 79560f3..17c8ed6 100644
--- a/extensions/guacamole-auth-ldap/pom.xml
+++ b/extensions/guacamole-auth-ldap/pom.xml
@@ -141,11 +141,17 @@
             <scope>provided</scope>
         </dependency>
 
-        <!-- JLDAP -->
+        <!-- Apache Directory LDAP API -->
         <dependency>
-            <groupId>com.novell.ldap</groupId>
-            <artifactId>jldap</artifactId>
-            <version>4.3</version>
+            <groupId>org.apache.directory.api</groupId>
+            <artifactId>api-all</artifactId>
+            <version>2.0.0.AM4</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <!-- Guice -->
@@ -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..49a3f7c 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPConnectionService.java
@@ -20,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,103 @@
      * @throws GuacamoleException
      *     If an error occurs while binding to the LDAP server.
      */
-    public LDAPConnection bindAs(String userDN, String password)
+    public LdapNetworkConnection bindAs(Dn userDN, String password)
             throws GuacamoleException {
 
-        // Obtain appropriately-configured LDAPConnection instance
-        LDAPConnection ldapConnection = createLDAPConnection();
-
-        // Configure LDAP connection constraints
-        LDAPConstraints ldapConstraints = ldapConnection.getConstraints();
-        if (ldapConstraints == null)
-          ldapConstraints = new LDAPConstraints();
-
-        // Set whether or not we follow referrals
-        ldapConstraints.setReferralFollowing(confService.getFollowReferrals());
-
-        // Set referral authentication to use the provided credentials.
-        if (userDN != null && !userDN.isEmpty())
-            ldapConstraints.setReferralHandler(new ReferralAuthHandler(userDN, password));
-
-        // Set the maximum number of referrals we follow
-        ldapConstraints.setHopLimit(confService.getMaxReferralHops());
-
-        // Set timelimit to wait for LDAP operations, converting to ms
-        ldapConstraints.setTimeLimit(confService.getOperationTimeout() * 1000);
-
-        // Apply the constraints to the connection
-        ldapConnection.setConstraints(ldapConstraints);
-
-        try {
+        // Get ldapConnection and try to connect and bind.
+        try (LdapNetworkConnection ldapConnection = createLDAPConnection()) {
 
             // Connect to LDAP server
-            ldapConnection.connect(
-                confService.getServerHostname(),
-                confService.getServerPort()
-            );
+            ldapConnection.connect();
 
             // Explicitly start TLS if requested
             if (confService.getEncryptionMethod() == EncryptionMethod.STARTTLS)
-                ldapConnection.startTLS();
+                ldapConnection.startTls();
 
-        }
-        catch (LDAPException e) {
-            logger.error("Unable to connect to LDAP server: {}", e.getMessage());
-            logger.debug("Failed to connect to LDAP server.", e);
-            return null;
-        }
-
-        // Bind using provided credentials
-        try {
-
-            byte[] passwordBytes;
-            try {
-
-                // Convert password into corresponding byte array
-                if (password != null)
-                    passwordBytes = password.getBytes("UTF-8");
-                else
-                    passwordBytes = null;
-
-            }
-            catch (UnsupportedEncodingException e) {
-                logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage());
-                logger.debug("Support for UTF-8 (as required by Java spec) not found.", e);
-                disconnect(ldapConnection);
-                return null;
-            }
-
-            // Bind as user
-            ldapConnection.bind(LDAPConnection.LDAP_V3, userDN, passwordBytes);
+            // Bind using provided credentials
+            BindRequest bindRequest = new BindRequestImpl();
+            bindRequest.setDn(userDN);
+            bindRequest.setCredentials(password);
+            BindResponse bindResponse = ldapConnection.bind(bindRequest);
+            if (bindResponse.getLdapResult().getResultCode() == ResultCodeEnum.SUCCESS)
+                return ldapConnection;
+            
+            else
+                throw new GuacamoleInvalidCredentialsException("Error binding"
+                        + " to server: " + bindResponse.toString(),
+                        CredentialsInfo.USERNAME_PASSWORD);
 
         }
 
         // Disconnect if an error occurs during bind
-        catch (LDAPException e) {
-            logger.debug("LDAP bind failed.", e);
-            disconnect(ldapConnection);
-            return null;
+        catch (LdapException e) {
+            logger.debug("Unable to bind to LDAP server.", e);
+            throw new GuacamoleInvalidCredentialsException(
+                    "Unable to bind to the LDAP server.",
+                    CredentialsInfo.USERNAME_PASSWORD);
         }
 
-        return ldapConnection;
-
+    }
+    
+    /**
+     * Generate a new LdapNetworkConnection object for following a referral
+     * with the given LdapUrl, and copy the username and password
+     * from the original connection.
+     * 
+     * @param referralUrl
+     *     The LDAP URL to follow.
+     * 
+     * @param ldapConfig
+     *     The connection configuration to use to retrieve username and
+     *     password.
+     * 
+     * @param hop
+     *     The current hop number of this referral - once the configured
+     *     limit is reached, this method will throw an exception.
+     * 
+     * @return
+     *     A LdapNetworkConnection object that points at the location
+     *     specified in the referralUrl.
+     *     
+     * @throws GuacamoleException
+     *     If an error occurs parsing out the LdapUrl object or the
+     *     maximum number of referral hops is reached.
+     */
+    public LdapNetworkConnection getReferralConnection(LdapUrl referralUrl,
+            LdapConnectionConfig ldapConfig, int hop)
+            throws GuacamoleException {
+       
+        if (hop >= confService.getMaxReferralHops())
+            throw new GuacamoleServerException("Maximum number of referrals reached.");
+        
+        LdapConnectionConfig referralConfig = new LdapConnectionConfig();
+        
+        // Copy bind name and password from original config
+        referralConfig.setName(ldapConfig.getName());
+        referralConfig.setCredentials(ldapConfig.getCredentials());        
+        
+        // Look for host - if not there, bail out.
+        String host = referralUrl.getHost();
+        if (host == null || host.isEmpty())
+            throw new GuacamoleServerException("Referral URL contains no host.");
+       
+        referralConfig.setLdapHost(host);
+       
+        // Look for port, or assign a default.
+        int port = referralUrl.getPort();
+        if (port < 1)
+            referralConfig.setLdapPort(389);
+        else
+            referralConfig.setLdapPort(port);
+        
+        // Deal with SSL connections
+        if (referralUrl.getScheme().equals(LdapUrl.LDAPS_SCHEME))
+            referralConfig.setUseSsl(true);
+        else
+            referralConfig.setUseSsl(false);
+        
+        return new LdapNetworkConnection(referralConfig);
+        
     }
 
     /**
@@ -196,19 +231,53 @@
      * @param ldapConnection
      *     The LDAP connection to disconnect.
      */
-    public void disconnect(LDAPConnection ldapConnection) {
+    public void disconnect(LdapConnection ldapConnection) {
 
         // Attempt disconnect
         try {
-            ldapConnection.disconnect();
+            ldapConnection.close();
         }
 
         // Warn if disconnect unexpectedly fails
-        catch (LDAPException e) {
+        catch (IOException e) {
             logger.warn("Unable to disconnect from LDAP server: {}", e.getMessage());
             logger.debug("LDAP disconnect failed.", e);
         }
 
     }
+    
+    /**
+     * Generate a SearchRequest object using the given Base DN and filter
+     * and retrieving other properties from the LDAP configuration service.
+     * 
+     * @param baseDn
+     *     The LDAP Base DN at which to search the search.
+     * 
+     * @param filter
+     *     A string representation of a LDAP filter to use for the search.
+     * 
+     * @return
+     *     The properly-configured SearchRequest object.
+     * 
+     * @throws GuacamoleException
+     *     If an error occurs retrieving any of the configuration values.
+     */
+    public SearchRequest getSearchRequest(Dn baseDn, ExprNode filter)
+            throws GuacamoleException {
+        
+        SearchRequest searchRequest = new SearchRequestImpl();
+        searchRequest.setBase(baseDn);
+        searchRequest.setDerefAliases(confService.getDereferenceAliases());
+        searchRequest.setScope(SearchScope.SUBTREE);
+        searchRequest.setFilter(filter);
+        searchRequest.setSizeLimit(confService.getMaxResults());
+        searchRequest.setTimeLimit(confService.getOperationTimeout());
+        searchRequest.setTypesOnly(false);
+        
+        if (confService.getFollowReferrals())
+            searchRequest.followReferrals();
+        
+        return searchRequest;
+    }
 
 }
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
index 2196c2f..ebf9792 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ObjectQueryService.java
@@ -20,18 +20,29 @@
 package org.apache.guacamole.auth.ldap;
 
 import com.google.inject.Inject;
-import com.novell.ldap.LDAPAttribute;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
-import com.novell.ldap.LDAPException;
-import com.novell.ldap.LDAPReferralException;
-import com.novell.ldap.LDAPSearchResults;
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
+import org.apache.directory.api.ldap.model.cursor.CursorException;
+import org.apache.directory.api.ldap.model.cursor.SearchCursor;
+import org.apache.directory.api.ldap.model.entry.Attribute;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapException;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
+import org.apache.directory.api.ldap.model.filter.AndNode;
+import org.apache.directory.api.ldap.model.filter.EqualityNode;
+import org.apache.directory.api.ldap.model.filter.ExprNode;
+import org.apache.directory.api.ldap.model.filter.OrNode;
+import org.apache.directory.api.ldap.model.message.Referral;
+import org.apache.directory.api.ldap.model.message.SearchRequest;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.url.LdapUrl;
+import org.apache.directory.ldap.client.api.LdapConnectionConfig;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
 import org.apache.guacamole.net.auth.Identifiable;
@@ -50,19 +61,13 @@
     /**
      * Logger for this class.
      */
-    private final Logger logger = LoggerFactory.getLogger(ObjectQueryService.class);
-
+    private static final Logger logger = LoggerFactory.getLogger(ObjectQueryService.class);
+    
     /**
-     * Service for escaping parts of LDAP queries.
+     * Service for connecting to LDAP directory.
      */
     @Inject
-    private EscapingService escapingService;
-
-    /**
-     * Service for retrieving LDAP server configuration information.
-     */
-    @Inject
-    private ConfigurationService confService;
+    private LDAPConnectionService ldapService;
 
     /**
      * Returns the identifier of the object represented by the given LDAP
@@ -86,14 +91,18 @@
      *     The identifier of the object represented by the given LDAP entry, or
      *     null if no attributes declared as containing the identifier of the
      *     object are present on the entry.
+     * 
+     * @throws LdapInvalidAttributeValueException
+     *     If an error occurs retrieving the value of the identifier attribute.
      */
-    public String getIdentifier(LDAPEntry entry, Collection<String> attributes) {
+    public String getIdentifier(Entry entry, Collection<String> attributes) 
+            throws LdapInvalidAttributeValueException {
 
         // Retrieve the first value of the highest priority identifier attribute
         for (String identifierAttribute : attributes) {
-            LDAPAttribute identifier = entry.getAttribute(identifierAttribute);
+            Attribute identifier = entry.get(identifierAttribute);
             if (identifier != null)
-                return identifier.getStringValue();
+                return identifier.getString();
         }
 
         // No identifier attribute is present on the entry
@@ -125,42 +134,25 @@
      *     An LDAP query which will search for arbitrary LDAP objects having at
      *     least one of the given attributes set to the specified value.
      */
-    public String generateQuery(String filter,
+    public ExprNode generateQuery(ExprNode filter,
             Collection<String> attributes, String attributeValue) {
 
         // Build LDAP query for objects having at least one attribute and with
         // the given search filter
-        StringBuilder ldapQuery = new StringBuilder();
-        ldapQuery.append("(&");
-        ldapQuery.append(filter);
+        AndNode searchFilter = new AndNode();
+        searchFilter.addNode(filter);
 
         // Include all attributes within OR clause if there are more than one
-        if (attributes.size() > 1)
-            ldapQuery.append("(|");
-
+        OrNode attributeFilter = new OrNode();
+       
         // Add equality comparison for each possible attribute
-        for (String attribute : attributes) {
-            ldapQuery.append("(");
-            ldapQuery.append(escapingService.escapeLDAPSearchFilter(attribute));
+        attributes.forEach(attribute ->
+            attributeFilter.addNode(new EqualityNode(attribute, attributeValue))
+        );
 
-            if (attributeValue != null) {
-                ldapQuery.append("=");
-                ldapQuery.append(escapingService.escapeLDAPSearchFilter(attributeValue));
-                ldapQuery.append(")");
-            }
-            else
-                ldapQuery.append("=*)");
-
-        }
-
-        // Close OR clause, if any
-        if (attributes.size() > 1)
-            ldapQuery.append(")");
-
-        // Close overall query (AND clause)
-        ldapQuery.append(")");
-
-        return ldapQuery.toString();
+        searchFilter.addNode(attributeFilter);
+        
+        return searchFilter;
 
     }
 
@@ -178,6 +170,10 @@
      *
      * @param query
      *     The LDAP query to execute.
+     * 
+     * @param searchHop
+     *     The current level of referral depth for this search, used for
+     *     limiting the maximum depth to which referrals can go.
      *
      * @return
      *     A list of all results accessible to the user currently bound under
@@ -188,43 +184,40 @@
      *     information required to execute the query cannot be read from
      *     guacamole.properties.
      */
-    public List<LDAPEntry> search(LDAPConnection ldapConnection,
-            String baseDN, String query) throws GuacamoleException {
+    public List<Entry> search(LdapNetworkConnection ldapConnection,
+            Dn baseDN, ExprNode query, int searchHop) throws GuacamoleException {
 
         logger.debug("Searching \"{}\" for objects matching \"{}\".", baseDN, query);
 
-        try {
+        LdapConnectionConfig ldapConnectionConfig = ldapConnection.getConfig();
+            
+        // Search within subtree of given base DN
+        SearchRequest request = ldapService.getSearchRequest(baseDN,
+                query);
+            
+        // Produce list of all entries in the search result, automatically
+        // following referrals if configured to do so
+        List<Entry> entries = new ArrayList<>();
+            
+        try (SearchCursor results = ldapConnection.search(request)) {
+            while (results.next()) {
 
-            // Search within subtree of given base DN
-            LDAPSearchResults results = ldapConnection.search(baseDN,
-                    LDAPConnection.SCOPE_SUB, query, null, false,
-                    confService.getLDAPSearchConstraints());
-
-            // Produce list of all entries in the search result, automatically
-            // following referrals if configured to do so
-            List<LDAPEntry> entries = new ArrayList<>(results.getCount());
-            while (results.hasMore()) {
-
-                try {
-                    entries.add(results.next());
+                if (results.isEntry()) {
+                    entries.add(results.getEntry());
                 }
+                else if (results.isReferral() && request.isFollowReferrals()) {
 
-                // Warn if referrals cannot be followed
-                catch (LDAPReferralException e) {
-                    if (confService.getFollowReferrals()) {
-                        logger.error("Could not follow referral: {}", e.getFailedReferral());
-                        logger.debug("Error encountered trying to follow referral.", e);
-                        throw new GuacamoleServerException("Could not follow LDAP referral.", e);
+                    Referral referral = results.getReferral();
+                    for (String url : referral.getLdapUrls()) {
+                        LdapNetworkConnection referralConnection =
+                                ldapService.getReferralConnection(
+                                        new LdapUrl(url),
+                                        ldapConnectionConfig, searchHop++
+                                );
+                        entries.addAll(search(referralConnection, baseDN, query,
+                                searchHop));
                     }
-                    else {
-                        logger.warn("Given a referral, but referrals are disabled. Error was: {}", e.getMessage());
-                        logger.debug("Got a referral, but configured to not follow them.", e);
-                    }
-                }
-                
-                catch (LDAPException e) {
-                  logger.warn("Failed to process an LDAP search result. Error was: {}", e.resultCodeToString());
-                  logger.debug("Error processing LDAPEntry search result.", e);
+
                 }
 
             }
@@ -232,7 +225,7 @@
             return entries;
 
         }
-        catch (LDAPException | GuacamoleException e) {
+        catch (CursorException | IOException | LdapException e) {
             throw new GuacamoleServerException("Unable to query list of "
                     + "objects from LDAP directory.", e);
         }
@@ -274,11 +267,11 @@
      *     information required to execute the query cannot be read from
      *     guacamole.properties.
      */
-    public List<LDAPEntry> search(LDAPConnection ldapConnection, String baseDN,
-            String filter, Collection<String> attributes, String attributeValue)
+    public List<Entry> search(LdapNetworkConnection ldapConnection, Dn baseDN,
+            ExprNode filter, Collection<String> attributes, String attributeValue)
             throws GuacamoleException {
-        String query = generateQuery(filter, attributes, attributeValue);
-        return search(ldapConnection, baseDN, query);
+        ExprNode query = generateQuery(filter, attributes, attributeValue);
+        return search(ldapConnection, baseDN, query, 0);
     }
 
     /**
@@ -302,15 +295,15 @@
      *     {@link Map} under its corresponding identifier.
      */
     public <ObjectType extends Identifiable> Map<String, ObjectType>
-        asMap(List<LDAPEntry> entries, Function<LDAPEntry, ObjectType> mapper) {
+        asMap(List<Entry> entries, Function<Entry, ObjectType> mapper) {
 
         // Convert each entry to the corresponding Guacamole API object
         Map<String, ObjectType> objects = new HashMap<>(entries.size());
-        for (LDAPEntry entry : entries) {
+        for (Entry entry : entries) {
 
             ObjectType object = mapper.apply(entry);
             if (object == null) {
-                logger.debug("Ignoring object \"{}\".", entry.getDN());
+                logger.debug("Ignoring object \"{}\".", entry.getDn().toString());
                 continue;
             }
 
@@ -320,7 +313,7 @@
             if (objects.putIfAbsent(identifier, object) != null)
                 logger.warn("Multiple objects ambiguously map to the "
                         + "same identifier (\"{}\"). Ignoring \"{}\" as "
-                        + "a duplicate.", identifier, entry.getDN());
+                        + "a duplicate.", identifier, entry.getDn().toString());
 
         }
 
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/ReferralAuthHandler.java
deleted file mode 100644
index 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/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/src/main/java/org/apache/guacamole/auth/openid/form/TokenField.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/form/TokenField.java
index 4a3bc9d..b6ca18b 100644
--- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/form/TokenField.java
+++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/form/TokenField.java
@@ -78,7 +78,7 @@
         this.authorizationURI = UriBuilder.fromUri(authorizationEndpoint)
                 .queryParam("scope", scope)
                 .queryParam("response_type", "id_token")
-                .queryParam("client_id","clientID")
+                .queryParam("client_id", clientID)
                 .queryParam("redirect_uri", redirectURI)
                 .queryParam("nonce", nonce)
                 .build();
diff --git a/extensions/guacamole-auth-openid/src/main/resources/config/openidConfig.js b/extensions/guacamole-auth-openid/src/main/resources/config/openidConfig.js
index 12bc0da..5d0b6b2 100644
--- a/extensions/guacamole-auth-openid/src/main/resources/config/openidConfig.js
+++ b/extensions/guacamole-auth-openid/src/main/resources/config/openidConfig.js
@@ -31,24 +31,3 @@
     });
 
 }]);
-
-/**
- * Config block which augments the existing routing, providing special handling
- * for the "id_token=" fragments provided by OpenID Connect.
- */
-angular.module('index').config(['$routeProvider',
-        function indexRouteConfig($routeProvider) {
-
-    // Transform "/#/id_token=..." to "/#/?id_token=..."
-    $routeProvider.when('/id_token=:response', {
-
-        template   : '',
-        controller : ['$location', function reroute($location) {
-            var params = $location.path().substring(1);
-            $location.url('/');
-            $location.search(params);
-        }]
-
-    });
-
-}]);
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 4bac126..b15f83f 100644
--- a/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
@@ -10,7 +10,8 @@
     ],
 
     "translations" : [
-        "translations/en.json"
+        "translations/en.json",
+        "translations/ja.json"
     ],
 
     "js" : [
diff --git a/extensions/guacamole-auth-openid/src/main/resources/transformToken.js b/extensions/guacamole-auth-openid/src/main/resources/transformToken.js
new file mode 100644
index 0000000..b65d2fd
--- /dev/null
+++ b/extensions/guacamole-auth-openid/src/main/resources/transformToken.js
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+
+/**
+ * Before AngularJS routing takes effect, reformat the URL fragment
+ * from the format used by OpenID Connect ("#param1=value1&param2=value2&...")
+ * to the format used by AngularJS ("#/?param1=value1&param2=value2&...") such
+ * that the client side of Guacamole's authentication system will automatically
+ * forward the "id_token" value for server-side validation.
+ * 
+ * Note that not all OpenID identity providers will include the "id_token"
+ * parameter in the first position; it may occur after several other parameters
+ * within the fragment.
+ */
+(function guacOpenIDTransformToken() {
+    if (/^#(?![?\/])(.*&)?id_token=/.test(location.hash))
+        location.hash = '/?' + location.hash.substring(1);
+})();
diff --git a/extensions/guacamole-auth-openid/src/main/resources/translations/ja.json b/extensions/guacamole-auth-openid/src/main/resources/translations/ja.json
new file mode 100644
index 0000000..9a5b518
--- /dev/null
+++ b/extensions/guacamole-auth-openid/src/main/resources/translations/ja.json
@@ -0,0 +1,7 @@
+{
+
+    "LOGIN" : {
+        "INFO_REDIRECT_PENDING" : "IDプロバイダへリダイレクトしています。"
+    }
+
+}
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 10c4f8a..48eeb42 100644
--- a/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
@@ -21,7 +21,8 @@
     ],
 
     "translations" : [
-        "translations/en.json"
+        "translations/en.json",
+        "translations/ja.json"
     ],
 
     "resources" : {
diff --git a/extensions/guacamole-auth-quickconnect/src/main/resources/translations/ja.json b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/ja.json
new file mode 100644
index 0000000..4d44d4b
--- /dev/null
+++ b/extensions/guacamole-auth-quickconnect/src/main/resources/translations/ja.json
@@ -0,0 +1,14 @@
+{
+
+    "QUICKCONNECT" : {
+        "ACTION_CONNECT"        : "接続",
+        
+        "ERROR_INVALID_URI"      : "URIが無効です",
+        "ERROR_NO_HOST"          : "ホストが指定されていません",
+        "ERROR_NO_PROTOCOL"      : "プロトコルが指定されていません",
+        "ERROR_NOT_ABSOLUTE_URI" : "絶対URIで指定してください",
+        
+        "FIELD_PLACEHOLDER_URI" : "接続するURIを入力"
+    }
+
+}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java
index 4fd37f1..fee4357 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/AuthenticationProviderService.java
@@ -25,7 +25,7 @@
 import java.util.Arrays;
 import javax.servlet.http.HttpServletRequest;
 import org.apache.guacamole.auth.radius.user.AuthenticatedUser;
-import org.apache.guacamole.auth.radius.form.RadiusChallengeResponseField;
+import org.apache.guacamole.auth.radius.form.GuacamoleRadiusChallenge;
 import org.apache.guacamole.auth.radius.form.RadiusStateField;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.form.Field;
@@ -42,6 +42,7 @@
 import net.jradius.packet.AccessChallenge;
 import net.jradius.packet.AccessReject;
 import net.jradius.packet.attribute.RadiusAttribute;
+import org.apache.guacamole.form.PasswordField;
 
 /**
  * Service providing convenience functions for the RADIUS AuthenticationProvider
@@ -53,6 +54,12 @@
      * Logger for this class.
      */
     private final Logger logger = LoggerFactory.getLogger(AuthenticationProviderService.class);
+    
+    /**
+     * The name of the password field where the user will enter a response to
+     * the RADIUS challenge.
+     */
+    private static final String CHALLENGE_RESPONSE_PARAM = "radiusChallenge";
 
     /**
      * Service for creating and managing connections to RADIUS servers.
@@ -67,18 +74,23 @@
     private Provider<AuthenticatedUser> authenticatedUserProvider;
 
     /**
-     * Returns the expected credentials from a RADIUS challenge.
+     * Returns an object containing the challenge message and the expected
+     * credentials from a RADIUS challenge, or null if either state or reply
+     * attributes are missing from the challenge.
      *
      * @param challengePacket
      *     The AccessChallenge RadiusPacket received from the RADIUS 
      *     server.
      *
      * @return
-     *     A CredentialsInfo object that represents fields that need to
-     *     be presented to the user in order to complete authentication.
-     *     One of these must be the RADIUS state.
+     *     A GuacamoleRadiusChallenge object that contains the challenge message
+     *     sent by the RADIUS server and the expected credentials that should
+     *     be requested of the user in order to continue authentication.  One
+     *     of the expected credentials *must* be the RADIUS state.  If either
+     *     state or the reply are missing from the challenge this method will
+     *     return null.
      */
-    private CredentialsInfo getRadiusChallenge(RadiusPacket challengePacket) {
+    private GuacamoleRadiusChallenge getRadiusChallenge(RadiusPacket challengePacket) {
 
         // Try to get the state attribute - if it's not there, we have a problem
         RadiusAttribute stateAttr = challengePacket.findAttribute(Attr_State.TYPE);
@@ -97,13 +109,16 @@
         }
 
         // We have the required attributes - convert to strings and then generate the additional login box/field
-        String replyMsg = replyAttr.toString();
+        String replyMsg = replyAttr.getValue().toString();
         String radiusState = BaseEncoding.base16().encode(stateAttr.getValue().getBytes());
-        Field radiusResponseField = new RadiusChallengeResponseField(replyMsg);
+        Field radiusResponseField = new PasswordField(CHALLENGE_RESPONSE_PARAM);
         Field radiusStateField = new RadiusStateField(radiusState);
 
-        // Return the CredentialsInfo object that has the state and the expected response.
-        return new CredentialsInfo(Arrays.asList(radiusResponseField,radiusStateField));
+        // Return the GuacamoleRadiusChallenge object that has the state
+        // and the expected response.
+        return new GuacamoleRadiusChallenge(replyMsg,
+                new CredentialsInfo(Arrays.asList(radiusResponseField,
+                        radiusStateField)));
     }
 
     /**
@@ -134,7 +149,7 @@
 
         // Grab HTTP request object and a response to a challenge.
         HttpServletRequest request = credentials.getRequest();
-        String challengeResponse = request.getParameter(RadiusChallengeResponseField.PARAMETER_NAME);
+        String challengeResponse = request.getParameter(CHALLENGE_RESPONSE_PARAM);
 
         // RadiusPacket object to store response from server.
         RadiusPacket radPack;
@@ -200,12 +215,14 @@
 
         // Received AccessChallenge packet, more credentials required to complete authentication
         else if (radPack instanceof AccessChallenge) {
-            CredentialsInfo expectedCredentials = getRadiusChallenge(radPack);
+            GuacamoleRadiusChallenge challenge = getRadiusChallenge(radPack);
 
-            if (expectedCredentials == null)
+            if (challenge == null)
                 throw new GuacamoleInvalidCredentialsException("Authentication error.", CredentialsInfo.USERNAME_PASSWORD);
 
-            throw new GuacamoleInsufficientCredentialsException("LOGIN.INFO_RADIUS_ADDL_REQUIRED", expectedCredentials);
+            throw new GuacamoleInsufficientCredentialsException(
+                    challenge.getChallengeText(),
+                    challenge.getExpectedCredentials());
         }
 
         // Something unanticipated happened, so panic and go back to login.
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java
new file mode 100644
index 0000000..4589794
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.radius.form;
+
+import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
+
+/**
+ * Stores the RADIUS challenge message and expected credentials in a single
+ * object.
+ */
+public class GuacamoleRadiusChallenge {
+    
+    /**
+     * The challenge text sent by the RADIUS server.
+     */
+    private final String challengeText;
+    
+    /**
+     * The expected credentials that need to be provided to satisfy the
+     * RADIUS authentication challenge.
+     */
+    private final CredentialsInfo expectedCredentials;
+    
+    /**
+     * Creates a new GuacamoleRadiusChallenge object with the provided
+     * challenge message and expected credentials.
+     * 
+     * @param challengeText
+     *     The challenge message sent by the RADIUS server.
+     * 
+     * @param expectedCredentials 
+     *     The credentials required to complete the challenge.
+     */
+    public GuacamoleRadiusChallenge(String challengeText,
+            CredentialsInfo expectedCredentials) {
+        this.challengeText = challengeText;
+        this.expectedCredentials = expectedCredentials;
+    }
+    
+    /**
+     * Returns the challenge message provided by the RADIUS server.
+     * 
+     * @return
+     *     The challenge message provided by the RADIUS server.
+     */
+    public String getChallengeText() {
+        return challengeText;
+    }
+    
+    /**
+     * Returns the credentials required to satisfy the RADIUS challenge.
+     * 
+     * @return 
+     *     The credentials required to satisfy the RADIUS challenge.
+     */
+    public CredentialsInfo getExpectedCredentials() {
+        return expectedCredentials;
+    }
+    
+}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java
deleted file mode 100644
index 32ceb90..0000000
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.guacamole.auth.radius.form;
-
-import org.apache.guacamole.form.Field;
-
-/**
- * A form used to prompt the user for additional information when
- * the RADIUS server sends a challenge back to the user with a reply
- * message.
- */
-public class RadiusChallengeResponseField extends Field {
-
-    /**
-     * The field returned by the RADIUS challenge/response.
-     */
-    public static final String PARAMETER_NAME = "guac-radius-challenge-response";
-
-    /**
-     * The type of field to initialize for the challenge/response.
-     */
-    private static final String RADIUS_FIELD_TYPE = "GUAC_RADIUS_CHALLENGE_RESPONSE";
-
-    /**
-     * The message the RADIUS server sent back in the challenge.
-     */
-    private final String challenge;
-
-    /**
-     * Initialize the field with the challenge sent back by the RADIUS server.
-     *
-     * @param challenge
-     *     The challenge message sent back by the RADIUS server.
-     */
-    public RadiusChallengeResponseField(String challenge) {
-        super(PARAMETER_NAME, RADIUS_FIELD_TYPE);
-        this.challenge = challenge;
-
-    }
-
-    /**
-     * Get the challenge sent by the RADIUS server.
-     *
-     * @return
-     *     A String that indicates the challenge returned
-     *     by the RADIUS server.
-     */
-    public String getChallenge() {
-        return challenge;
-    }
-}
diff --git a/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js b/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
index dab0ffc..a3d72bf 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
+++ b/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
@@ -23,13 +23,6 @@
 angular.module('guacRadius').config(['formServiceProvider',
         function guacRadiusConfig(formServiceProvider) {
 
-    // Define field for the challenge from the RADIUS service
-    formServiceProvider.registerFieldType('GUAC_RADIUS_CHALLENGE_RESPONSE', {
-        module      : 'guacRadius',
-        controller  : 'radiusResponseController',
-        templateUrl : 'app/ext/radius/templates/radiusResponseField.html'
-    });
-
     // Define the hidden field for the RADIUS state
     formServiceProvider.registerFieldType('GUAC_RADIUS_STATE', {
         module      : 'guacRadius',
diff --git a/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js b/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js
deleted file mode 100644
index 4782b20..0000000
--- a/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/**
- * Controller for the "GUAC_RADIUS_CHALLENGE_RESPONSE" field which
- * passes the RADIUS server challenge to the user and takes the response.
- */
-angular.module('guacRadius').controller('radiusResponseController', ['$scope', '$injector',
-        function radiusResponseController($scope, $injector) {
-
-    // Populate the reply message field
-    $scope.radiusPlaceholder = $scope.field.challenge;
-
-}]);
diff --git a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
index ac1116b..707f233 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
@@ -10,7 +10,8 @@
     ],
 
     "translations" : [
-        "translations/en.json"
+        "translations/en.json",
+        "translations/ja.json"
     ],
 
     "js" : [
@@ -18,7 +19,6 @@
     ],
 
     "resources" : {
-        "templates/radiusResponseField.html" : "text/html",
         "templates/radiusStateField.html"    : "text/html"
     }
 
diff --git a/extensions/guacamole-auth-radius/src/main/resources/templates/radiusResponseField.html b/extensions/guacamole-auth-radius/src/main/resources/templates/radiusResponseField.html
deleted file mode 100644
index eec760f..0000000
--- a/extensions/guacamole-auth-radius/src/main/resources/templates/radiusResponseField.html
+++ /dev/null
@@ -1 +0,0 @@
-<input type="password" ng-model="model" ng-trim="false" autocorrect="off" autocapitalize="off" placeholder="{{radiusPlaceholder}}" />
diff --git a/extensions/guacamole-auth-radius/src/main/resources/translations/en.json b/extensions/guacamole-auth-radius/src/main/resources/translations/en.json
index c068a70..66232e4 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/translations/en.json
+++ b/extensions/guacamole-auth-radius/src/main/resources/translations/en.json
@@ -5,9 +5,7 @@
     },
 
     "LOGIN" : {
-        "FIELD_HEADER_GUAC_RADIUS_CHALLENGE_RESPONSE" : "",
-        "FIELD_HEADER_GUAC_RADIUS_STATE"              : "",
-        "INFO_RADIUS_ADDL_REQUIRED"                   : "Please supply additional credentials"
+        "FIELD_HEADER_GUAC_RADIUS_STATE"              : ""
     }
 
 }
diff --git a/extensions/guacamole-auth-radius/src/main/resources/translations/ja.json b/extensions/guacamole-auth-radius/src/main/resources/translations/ja.json
new file mode 100644
index 0000000..584f31b
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/resources/translations/ja.json
@@ -0,0 +1,7 @@
+{
+
+    "LOGIN" : {
+        "INFO_RADIUS_ADDL_REQUIRED"                   : "追加の認証情報を入力してください"
+    }
+
+}
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 31dda60..ed42d6f 100644
--- a/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
@@ -10,7 +10,8 @@
     ],
 
     "translations" : [
-        "translations/en.json"
+        "translations/en.json",
+        "translations/ja.json"
     ],
 
     "js" : [
diff --git a/extensions/guacamole-auth-totp/src/main/resources/translations/ja.json b/extensions/guacamole-auth-totp/src/main/resources/translations/ja.json
new file mode 100644
index 0000000..a946ff4
--- /dev/null
+++ b/extensions/guacamole-auth-totp/src/main/resources/translations/ja.json
@@ -0,0 +1,26 @@
+{
+
+    "TOTP" : {
+
+        "ACTION_HIDE_DETAILS" : "非表示",
+        "ACTION_SHOW_DETAILS" : "表示",
+
+        "FIELD_HEADER_ALGORITHM"  : "アルゴリズム:",
+        "FIELD_HEADER_DIGITS"     : "認証コード桁数:",
+        "FIELD_HEADER_INTERVAL"   : "認証コード利用可能時間(秒):",
+        "FIELD_HEADER_SECRET_KEY" : "秘密鍵:",
+
+        "FIELD_PLACEHOLDER_CODE" : "認証コード",
+
+        "INFO_CODE_REQUIRED"       : "認証コードを入力してください。",
+        "INFO_ENROLL_REQUIRED"     : "二要素認証システムが有効になっています。",
+        "INFO_VERIFICATION_FAILED" : "認証に失敗しました。もう一度やり直してください。",
+
+        "HELP_ENROLL_BARCODE" : "スマートフォンやタブレット等のデバイスの二要素認証アプリでQRコードを読み込んでください。",
+        "HELP_ENROLL_VERIFY"  : "QRコードを読み込み、表示された {DIGITS}桁の認証コードを入力してください。",
+
+        "SECTION_HEADER_DETAILS" : "詳細:"
+
+    }
+
+}
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/Tunnel.js b/guacamole-common-js/src/main/webapp/modules/Tunnel.js
index 4f056c9..149dce4 100644
--- a/guacamole-common-js/src/main/webapp/modules/Tunnel.js
+++ b/guacamole-common-js/src/main/webapp/modules/Tunnel.js
@@ -1015,7 +1015,7 @@
                     var opcode = elements.shift();
 
                     // Update state and UUID when first instruction received
-                    if (tunnel.state === Guacamole.Tunnel.State.CONNECTING) {
+                    if (tunnel.uuid === null) {
 
                         // Associate tunnel UUID if received
                         if (opcode === Guacamole.Tunnel.INTERNAL_DATA_OPCODE)
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/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 b2adfb7..932cd85 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",
@@ -98,6 +99,7 @@
                         "failsafe",
                         "fr-fr-azerty",
                         "fr-ch-qwertz",
+                        "hu-hu-qwertz",                        
                         "it-it-qwerty",
                         "ja-jp-qwerty",
                         "pt-br-qwerty",
@@ -107,6 +109,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 8b7e80b..a1d01ed 100644
--- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json
@@ -47,7 +47,7 @@
             "fields" : [
                 {
                     "name"  : "color-scheme",
-                    "type"  : "TEXT",
+                    "type"  : "TERMINAL_COLOR_SCHEME",
                     "options" : [ "", "black-white", "gray-black", "green-black", "white-black" ]
                 },
                 {
@@ -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" ]
@@ -91,6 +95,14 @@
                     "type"  : "TEXT"
                 },
                 {
+                    "name"  : "locale",
+                    "type"  : "TEXT"
+                },
+                {
+                    "name"  : "timezone",
+                    "type"  : "TIMEZONE"
+                },
+                {
                     "name"  : "server-alive-interval",
                     "type"  : "NUMERIC"
                 }
@@ -106,7 +118,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 81d74e3..0f70f05 100644
--- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/telnet.json
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/telnet.json
@@ -51,7 +51,7 @@
             "fields" : [
                 {
                     "name"  : "color-scheme",
-                    "type"  : "TEXT",
+                    "type"  : "TERMINAL_COLOR_SCHEME",
                     "options" : [ "", "black-white", "gray-black", "green-black", "white-black" ]
                 },
                 {
@@ -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 3834b63..d7cf465 100644
--- a/guacamole/pom.xml
+++ b/guacamole/pom.xml
@@ -501,6 +501,20 @@
             <version>27.0.1-jre</version>
         </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>
 
 </project>
diff --git a/guacamole/src/licenses/LICENSE b/guacamole/src/licenses/LICENSE
index f1e93b7..2995811 100644
--- a/guacamole/src/licenses/LICENSE
+++ b/guacamole/src/licenses/LICENSE
@@ -614,6 +614,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/)
 --------------------------------
 
@@ -667,6 +697,37 @@
 WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 
 
+Pickr (https://simonwep.github.io/pickr/)
+-----------------------------------------
+
+    Version: 1.2.6
+    From: 'Simon Reinisch' (https://github.com/Simonwep/)
+    License(s):
+        MIT (bundled/pickr-1.2.6/LICENSE)
+
+MIT License
+
+Copyright (c) 2019 Simon Reinisch
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
 Simple Logging Facade for Java (http://slf4j.org/)
 --------------------------------------------------
 
diff --git a/guacamole/src/licenses/bundled/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/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 5ee084b..c8941b3 100644
--- a/guacamole/src/main/webapp/app/client/controllers/clientController.js
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -28,6 +28,7 @@
     var ManagedClient      = $injector.get('ManagedClient');
     var ManagedClientState = $injector.get('ManagedClientState');
     var ManagedFilesystem  = $injector.get('ManagedFilesystem');
+    var Protocol           = $injector.get('Protocol');
     var ScrollState        = $injector.get('ScrollState');
 
     // Required services
@@ -251,7 +252,15 @@
          *
          * @type ScrollState
          */
-        scrollState : new ScrollState()
+        scrollState : new ScrollState(),
+
+        /**
+         * The current desired values of all editable connection parameters as
+         * a set of name/value pairs, including any changes made by the user.
+         *
+         * @type {Object.<String, String>}
+         */
+        connectionParameters : {}
 
     };
 
@@ -261,6 +270,16 @@
     };
 
     /**
+     * Applies any changes to connection parameters made by the user within the
+     * Guacamole menu.
+     */
+    $scope.applyParameterChanges = function applyParameterChanges() {
+        angular.forEach($scope.menu.connectionParameters, function sendArgv(value, name) {
+            ManagedClient.setArgument($scope.client, name, value);
+        });
+    };
+
+    /**
      * The client which should be attached to the client UI.
      *
      * @type ManagedClient
@@ -280,9 +299,12 @@
     })(guacClientManager.getManagedClients());
 
     /**
-     * Map of data source identifier to the root connection group of that data
-     * source, or null if the connection group hierarchy has not yet been
-     * loaded.
+     * The root connection groups of the connection hierarchy that should be
+     * presented to the user for selecting a different connection, as a map of
+     * data source identifier to the root connection group of that data
+     * source. This will be null if the connection group hierarchy has not yet
+     * been loaded or if the hierarchy is inapplicable due to only one
+     * connection or balancing group being available.
      *
      * @type Object.<String, ConnectionGroup>
      */
@@ -313,7 +335,13 @@
         ConnectionGroup.ROOT_IDENTIFIER
     )
     .then(function rootGroupsRetrieved(rootConnectionGroups) {
-        $scope.rootConnectionGroups = rootConnectionGroups;
+
+        // Store retrieved groups only if there are multiple connections or
+        // balancing groups available
+        var clientPages = userPageService.getClientPages(rootConnectionGroups);
+        if (clientPages.length > 1)
+            $scope.rootConnectionGroups = rootConnectionGroups;
+
     }, requestService.WARN);
 
     /**
@@ -481,12 +509,20 @@
 
     });
 
+    // Update client state/behavior as visibility of the Guacamole menu changes
     $scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) {
         
-        // Send clipboard data if menu is hidden
-        if (!menuShown && menuShownPreviousState)
+        // Send clipboard and argument value data once menu is hidden
+        if (!menuShown && menuShownPreviousState) {
             $scope.$broadcast('guacClipboard', $scope.client.clipboardData);
-        
+            $scope.applyParameterChanges();
+        }
+
+        // Obtain snapshot of current editable connection parameters when menu
+        // is opened
+        else if (menuShown)
+            $scope.menu.connectionParameters = ManagedClient.getArgumentModel($scope.client);
+
         // Disable client keyboard if the menu is shown
         $scope.client.clientProperties.keyboardEnabled = !menuShown;
 
@@ -864,6 +900,11 @@
     $scope.clientMenuActions = [ DISCONNECT_MENU_ACTION ];
 
     /**
+     * @borrows Protocol.getNamespace
+     */
+    $scope.getProtocolNamespace = Protocol.getNamespace;
+
+    /**
      * The currently-visible filesystem within the filesystem menu, if the
      * filesystem menu is open. If no filesystem is currently visible, this
      * will be null.
diff --git a/guacamole/src/main/webapp/app/client/styles/connection-select-menu.css b/guacamole/src/main/webapp/app/client/styles/connection-select-menu.css
new file mode 100644
index 0000000..3abfaa4
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/styles/connection-select-menu.css
@@ -0,0 +1,57 @@
+/*
+ * 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.
+ */
+
+#guac-menu .header h2.connection-select-menu {
+    overflow: visible;
+}
+
+.connection-select-menu {
+    padding: 0;
+    min-width: 0;
+}
+
+.connection-select-menu .menu-dropdown {
+    border: none;
+}
+
+.connection-select-menu .menu-dropdown .menu-contents {
+    font-weight: normal;
+    font-size: 0.8em;
+    right: auto;
+    left: 0;
+    max-width: 100vw;
+    width: 400px;
+}
+
+.connection-select-menu .menu-dropdown .menu-contents .filter input {
+    border-bottom: 1px solid rgba(0,0,0,0.125);
+    border-left: none;
+}
+
+.connection-select-menu .menu-dropdown .menu-contents .filter {
+    margin-bottom: 0.5em;
+    padding: 0;
+}
+
+.connection-select-menu .menu-dropdown .menu-contents .group-list .caption {
+    display: inline-block;
+    width: 100%;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
diff --git a/guacamole/src/main/webapp/app/client/styles/guac-menu.css b/guacamole/src/main/webapp/app/client/styles/guac-menu.css
index 48fb3b3..aa80e09 100644
--- a/guacamole/src/main/webapp/app/client/styles/guac-menu.css
+++ b/guacamole/src/main/webapp/app/client/styles/guac-menu.css
@@ -66,26 +66,10 @@
 }
 
 #guac-menu .header h2 {
-    padding: 0;
-}
-
-#guac-menu .header h2 .menu-dropdown {
-    border: none;
-}
-
-#guac-menu .header h2 .menu-contents {
-    font-weight: normal;
-    font-size: 0.8em;
-}
-
-#guac-menu .header .filter input {
-    border-bottom: 1px solid rgba(0,0,0,0.125);
-    border-left: none;
-}
-
-#guac-menu .header .filter {
-    margin-bottom: 0.5em;
-    padding: 0;
+    white-space: nowrap;
+    overflow: hidden;
+    width: 100%;
+    text-overflow: ellipsis;
 }
 
 #guac-menu #mouse-settings .choice {
diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html
index 9d06549..5325b47 100644
--- a/guacamole/src/main/webapp/app/client/templates/client.html
+++ b/guacamole/src/main/webapp/app/client/templates/client.html
@@ -53,8 +53,8 @@
             <!-- Stationary header -->
             <div class="header">
                 <h2 ng-hide="rootConnectionGroups">{{client.name}}</h2>
-                <h2 ng-show="rootConnectionGroups">
-                    <guac-menu menu-title="client.name">
+                <h2 class="connection-select-menu" ng-show="rootConnectionGroups">
+                    <guac-menu menu-title="client.name" interactive="true">
                         <div class="all-connections">
                             <guac-group-list-filter connection-groups="rootConnectionGroups"
                                 filtered-connection-groups="filteredRootConnectionGroups"
@@ -119,6 +119,14 @@
                     </div>
                 </div>
 
+                <!-- Connection parameters which may be modified while the connection is open -->
+                <div class="menu-section connection-parameters" id="connection-settings" ng-show="client.protocol">
+                    <guac-form namespace="getProtocolNamespace(client.protocol)"
+                               content="client.forms"
+                               model="menu.connectionParameters"
+                               model-only="true"></guac-form>
+                </div>
+
                 <!-- Input method -->
                 <div class="menu-section" id="keyboard-settings">
                     <h3>{{'CLIENT.SECTION_HEADER_INPUT_METHOD' | translate}}</h3>
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedArgument.js b/guacamole/src/main/webapp/app/client/types/ManagedArgument.js
new file mode 100644
index 0000000..247d9f6
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/types/ManagedArgument.js
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Provides the ManagedArgument class used by ManagedClient.
+ */
+angular.module('client').factory('ManagedArgument', ['$q', function defineManagedArgument($q) {
+
+    /**
+     * Object which represents an argument (connection parameter) which may be
+     * changed by the user while the connection is open.
+     * 
+     * @constructor
+     * @param {ManagedArgument|Object} [template={}]
+     *     The object whose properties should be copied within the new
+     *     ManagedArgument.
+     */
+    var ManagedArgument = function ManagedArgument(template) {
+
+        // Use empty object by default
+        template = template || {};
+
+        /**
+         * The name of the connection parameter.
+         *
+         * @type {String}
+         */
+        this.name = template.name;
+
+        /**
+         * The current value of the connection parameter.
+         *
+         * @type {String}
+         */
+        this.value = template.value;
+
+        /**
+         * A valid, open output stream which may be used to apply a new value
+         * to the connection parameter.
+         *
+         * @type {Guacamole.OutputStream}
+         */
+        this.stream = template.stream;
+
+    };
+
+    /**
+     * Requests editable access to a given connection parameter, returning a
+     * promise which is resolved with a ManagedArgument instance that provides
+     * such access if the parameter is indeed editable.
+     *
+     * @param {ManagedClient} managedClient
+     *     The ManagedClient instance associated with the connection for which
+     *     an editable version of the connection parameter is being retrieved.
+     *
+     * @param {String} name
+     *     The name of the connection parameter.
+     *
+     * @param {String} value
+     *     The current value of the connection parameter, as received from a
+     *     prior, inbound "argv" stream.
+     *
+     * @returns {Promise.<ManagedArgument>}
+     *     A promise which is resolved with the new ManagedArgument instance
+     *     once the requested parameter has been verified as editable.
+     */
+    ManagedArgument.getInstance = function getInstance(managedClient, name, value) {
+
+        var deferred = $q.defer();
+
+        // Create internal, fully-populated instance of ManagedArgument, to be
+        // returned only once mutability of the associated connection parameter
+        // has been verified
+        var managedArgument = new ManagedArgument({
+            name   : name,
+            value  : value,
+            stream : managedClient.client.createArgumentValueStream('text/plain', name)
+        });
+
+        // The connection parameter is editable only if a successful "ack" is
+        // received
+        managedArgument.stream.onack = function ackReceived(status) {
+            if (status.isError())
+                deferred.reject(status);
+            else
+                deferred.resolve(managedArgument);
+        };
+
+        return deferred.promise;
+
+    };
+
+    /**
+     * Sets the given editable argument (connection parameter) to the given
+     * value, updating the behavior of the associated connection in real-time.
+     * If successful, the ManagedArgument provided cannot be used for future
+     * calls to setValue() and must be replaced with a new instance. This
+     * function only has an effect if the new parameter value is different from
+     * the current value.
+     *
+     * @param {ManagedArgument} managedArgument
+     *     The ManagedArgument instance associated with the connection
+     *     parameter being modified.
+     *
+     * @param {String} value
+     *     The new value to assign to the connection parameter.
+     *
+     * @returns {Boolean}
+     *     true if the connection parameter was sent and the provided
+     *     ManagedArgument instance may no longer be used for future setValue()
+     *     calls, false if the connection parameter was NOT sent as it has not
+     *     changed.
+     */
+    ManagedArgument.setValue = function setValue(managedArgument, value) {
+
+        // Stream new value only if value has changed
+        if (value !== managedArgument.value) {
+
+            var writer = new Guacamole.StringWriter(managedArgument.stream);
+            writer.sendText(value);
+            writer.sendEnd();
+
+            // ManagedArgument instance is no longer usable
+            return true;
+
+        }
+
+        // No parameter value change was attempted and the ManagedArgument
+        // instance may be reused
+        return false;
+
+    };
+
+    return ManagedArgument;
+
+}]);
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
index 2889311..c1eccdd 100644
--- a/guacamole/src/main/webapp/app/client/types/ManagedClient.js
+++ b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
@@ -27,6 +27,7 @@
     var ClientProperties       = $injector.get('ClientProperties');
     var ClientIdentifier       = $injector.get('ClientIdentifier');
     var ClipboardData          = $injector.get('ClipboardData');
+    var ManagedArgument        = $injector.get('ManagedArgument');
     var ManagedClientState     = $injector.get('ManagedClientState');
     var ManagedClientThumbnail = $injector.get('ManagedClientThumbnail');
     var ManagedDisplay         = $injector.get('ManagedDisplay');
@@ -35,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
@@ -127,6 +131,23 @@
         this.title = template.title;
 
         /**
+         * The name which uniquely identifies the protocol of the connection in
+         * use. If the protocol cannot be determined, such as when a connection
+         * group is in use, this will be null.
+         *
+         * @type {String}
+         */
+        this.protocol = template.protocol || null;
+
+        /**
+         * An array of forms describing all known parameters for the connection
+         * in use, including those which may not be editable.
+         *
+         * @type {Form[]}
+         */
+        this.forms = template.forms || [];
+
+        /**
          * The most recently-generated thumbnail for this connection, as
          * stored within the local connection history. If no thumbnail is
          * stored, this will be null.
@@ -188,6 +209,17 @@
          */
         this.clientProperties = template.clientProperties || new ClientProperties();
 
+        /**
+         * All editable arguments (connection parameters), stored by their
+         * names. Arguments will only be present within this set if their
+         * current values have been exposed by the server via an inbound "argv"
+         * stream and the server has confirmed that the value may be changed
+         * through a successful "ack" to an outbound "argv" stream.
+         *
+         * @type {Object.<String, ManagedArgument>}
+         */
+        this.arguments = template.arguments || {};
+
     };
 
     /**
@@ -235,6 +267,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
@@ -456,6 +489,33 @@
 
         };
 
+        // Test for argument mutability whenever an argument value is
+        // received
+        client.onargv = function clientArgumentValueReceived(stream, mimetype, name) {
+
+            // Ignore arguments which do not use a mimetype currently supported
+            // by the web application
+            if (mimetype !== 'text/plain')
+                return;
+
+            var reader = new Guacamole.StringReader(stream);
+
+            // Assemble received data into a single string
+            var value = '';
+            reader.ontext = function textReceived(text) {
+                value += text;
+            };
+
+            // Test mutability once stream is finished, storing the current
+            // value for the argument only if it is mutable
+            reader.onend = function textComplete() {
+                ManagedArgument.getInstance(managedClient, name, value).then(function argumentIsMutable(argument) {
+                    managedClient.arguments[name] = argument;
+                }, function ignoreImmutableArguments() {});
+            };
+
+        };
+
         // Handle any received clipboard data
         client.onclipboard = function clientClipboardReceived(stream, mimetype) {
 
@@ -530,11 +590,16 @@
             client.connect(connectString);
         });
 
-        // If using a connection, pull connection name
+        // If using a connection, pull connection name and protocol information
         if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) {
-            connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id)
-            .then(function connectionRetrieved(connection) {
-                managedClient.name = managedClient.title = connection.name;
+            $q.all({
+                connection : connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id),
+                protocols  : schemaService.getProtocols(clientIdentifier.dataSource)
+            })
+            .then(function dataRetrieved(values) {
+                managedClient.name = managedClient.title = values.connection.name;
+                managedClient.protocol = values.connection.protocol;
+                managedClient.forms = values.protocols[values.connection.protocol].connectionForms;
             }, requestService.WARN);
         }
         
@@ -546,6 +611,29 @@
             }, requestService.WARN);
         }
 
+        // If using an active connection, pull corresponding connection, then
+        // pull connection name and protocol information from that
+        else if (clientIdentifier.type === ClientIdentifier.Types.ACTIVE_CONNECTION) {
+            activeConnectionService.getActiveConnection(clientIdentifier.dataSource, clientIdentifier.id)
+            .then(function activeConnectionRetrieved(activeConnection) {
+
+                // Attempt to retrieve connection details only if the
+                // underlying connection is known
+                if (activeConnection.connectionIdentifier) {
+                    $q.all({
+                        connection : connectionService.getConnection(clientIdentifier.dataSource, activeConnection.connectionIdentifier),
+                        protocols  : schemaService.getProtocols(clientIdentifier.dataSource)
+                    })
+                    .then(function dataRetrieved(values) {
+                        managedClient.name = managedClient.title = values.connection.name;
+                        managedClient.protocol = values.connection.protocol;
+                        managedClient.forms = values.protocols[values.connection.protocol].connectionForms;
+                    }, requestService.WARN);
+                }
+
+            }, requestService.WARN);
+        }
+
         return managedClient;
 
     };
@@ -628,6 +716,52 @@
     };
 
     /**
+     * Assigns the given value to the connection parameter having the given
+     * name, updating the behavior of the connection in real-time. If the
+     * connection parameter is not editable, this function has no effect.
+     *
+     * @param {ManagedClient} managedClient
+     *     The ManagedClient instance associated with the active connection
+     *     being modified.
+     *
+     * @param {String} name
+     *     The name of the connection parameter to modify.
+     *
+     * @param {String} value
+     *     The value to attempt to assign to the given connection parameter.
+     */
+    ManagedClient.setArgument = function setArgument(managedClient, name, value) {
+        var managedArgument = managedClient.arguments[name];
+        if (managedArgument && ManagedArgument.setValue(managedArgument, value))
+            delete managedClient.arguments[name];
+    };
+
+    /**
+     * Retrieves the current values of all editable connection parameters as a
+     * set of name/value pairs suitable for use as the model of a form which
+     * edits those parameters.
+     *
+     * @param {ManagedClient} client
+     *     The ManagedClient instance associated with the active connection
+     *     whose parameter values are being retrieved.
+     *
+     * @returns {Object.<String, String>}
+     *     A new set of name/value pairs containing the current values of all
+     *     editable parameters.
+     */
+    ManagedClient.getArgumentModel = function getArgumentModel(client) {
+
+        var model = {};
+
+        angular.forEach(client.arguments, function addModelEntry(managedArgument) {
+            model[managedArgument.name] = managedArgument.value;
+        });
+
+        return model;
+
+    };
+
+    /**
      * Produces a sharing link for the given ManagedClient using the given
      * sharing profile. The resulting sharing link, and any required login
      * information, can be retrieved from the <code>shareLinks</code> property
diff --git a/guacamole/src/main/webapp/app/element/directives/guacFocus.js b/guacamole/src/main/webapp/app/element/directives/guacFocus.js
index ce6093c..5087e7f 100644
--- a/guacamole/src/main/webapp/app/element/directives/guacFocus.js
+++ b/guacamole/src/main/webapp/app/element/directives/guacFocus.js
@@ -20,7 +20,11 @@
 /**
  * A directive which allows elements to be manually focused / blurred.
  */
-angular.module('element').directive('guacFocus', ['$parse', function guacFocus($parse) {
+angular.module('element').directive('guacFocus', ['$injector', function guacFocus($injector) {
+
+    // Required services
+    var $parse   = $injector.get('$parse');
+    var $timeout = $injector.get('$timeout');
 
     return {
         restrict: 'A',
@@ -44,7 +48,7 @@
 
             // Set/unset focus depending on value of guacFocus
             $scope.$watch(guacFocus, function updateFocus(value) {
-                $scope.$evalAsync(function updateFocusAsync() {
+                $timeout(function updateFocusAfterRender() {
                     if (value)
                         element.focus();
                     else
@@ -52,20 +56,6 @@
                 });
             });
 
-            // Set focus flag when focus is received
-            element.addEventListener('focus', function focusReceived() {
-                $scope.$evalAsync(function setGuacFocusAsync() {
-                    guacFocus.assign($scope, true);
-                });
-            });
-
-            // Unset focus flag when focus is lost
-            element.addEventListener('blur', function focusLost() {
-                $scope.$evalAsync(function unsetGuacFocusAsync() {
-                    guacFocus.assign($scope, false);
-                });
-            });
-
         } // end guacFocus link function
 
     };
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/form.js b/guacamole/src/main/webapp/app/form/directives/form.js
index 518db7d..81f500f 100644
--- a/guacamole/src/main/webapp/app/form/directives/form.js
+++ b/guacamole/src/main/webapp/app/form/directives/form.js
@@ -64,7 +64,22 @@
              *
              * @type Boolean
              */
-            modelOnly : '='
+            modelOnly : '=',
+
+            /**
+             * Whether the contents of the form should be rendered as disabled.
+             * By default, form fields are enabled.
+             *
+             * @type Boolean
+             */
+            disabled : '=',
+
+            /**
+             * The name of the field to be focused, if any.
+             *
+             * @type String
+             */
+            focused : '='
 
         },
         templateUrl: 'app/form/templates/form.html',
@@ -173,6 +188,19 @@
             });
 
             /**
+             * Returns whether the given field should be focused or not.
+             *
+             * @param {Field} field
+             *     The field to check.
+             *
+             * @returns {Boolean}
+             *     true if the given field should be focused, false otherwise.
+             */
+            $scope.isFocused = function isFocused(field) {
+                return field && (field.name === $scope.focused);
+            };
+
+            /**
              * Returns whether the given field should be displayed to the
              * current user.
              *
diff --git a/guacamole/src/main/webapp/app/form/directives/formField.js b/guacamole/src/main/webapp/app/form/directives/formField.js
index ea0f35f..fbc0cfe 100644
--- a/guacamole/src/main/webapp/app/form/directives/formField.js
+++ b/guacamole/src/main/webapp/app/form/directives/formField.js
@@ -53,7 +53,22 @@
              *
              * @type String
              */
-            model : '='
+            model : '=',
+
+            /**
+             * Whether this field should be rendered as disabled. By default,
+             * form fields are enabled.
+             *
+             * @type Boolean
+             */
+            disabled : '=',
+
+            /**
+             * Whether this field should be focused.
+             *
+             * @type Boolean
+             */
+            focused : '='
 
         },
         templateUrl: 'app/form/templates/formField.html',
@@ -73,6 +88,18 @@
             var fieldContent = $element.find('.form-field');
 
             /**
+             * An ID value which is reasonably likely to be unique relative to
+             * other elements on the page. This ID should be used to associate
+             * the relevant input element with the label provided by the
+             * guacFormField directive, if there is such an input element.
+             *
+             * @type String
+             */
+            $scope.fieldId = 'guac-field-XXXXXXXXXXXXXXXX'.replace(/X/g, function getRandomCharacter() {
+                return Math.floor(Math.random() * 36).toString(36);
+            }) + '-' + new Date().getTime().toString(36);
+
+            /**
              * Produces the translation string for the header of the current
              * field. The translation string will be of the form:
              *
@@ -158,4 +185,4 @@
         }] // end controller
     };
     
-}]);
\ No newline at end of file
+}]);
diff --git a/guacamole/src/main/webapp/app/form/directives/guacInputColor.js b/guacamole/src/main/webapp/app/form/directives/guacInputColor.js
new file mode 100644
index 0000000..3b010cc
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/directives/guacInputColor.js
@@ -0,0 +1,202 @@
+/*
+ * 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, leveraging the "Pickr"
+ * color picker. If the "Picker" color picker cannot be used because it relies
+ * on JavaScript features not supported by the browser (Internet Explorer), a
+ * "guacInputColorUnavailable" event will be emitted up the scope, and this
+ * directive will become read-only, functioning essentially as a color preview.
+ */
+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 $q         = $injector.get('$q');
+        var $translate = $injector.get('$translate');
+
+        /**
+         * Whether the color picker ("Pickr") cannot be used. In general, all
+         * browsers should support Pickr with the exception of Internet
+         * Explorer.
+         *
+         * @type Boolean
+         */
+        $scope.colorPickerUnavailable = false;
+
+        /**
+         * 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
+
+        };
+
+        // Init color picker after required translation strings are available
+        $q.all({
+            'save'   : $translate('APP.ACTION_SAVE'),
+            'cancel' : $translate('APP.ACTION_CANCEL')
+        }).then(function stringsRetrieved(strings) {
+
+            try {
+
+                /**
+                 * An instance of the "Pickr" color picker, bound to the underlying
+                 * element of this directive.
+                 *
+                 * @type Pickr
+                 */
+                var pickr = Pickr.create({
+
+                    // Bind color picker to the underlying element of this directive
+                    el : $element[0],
+
+                    // Wrap color picker dialog in Guacamole-specific class for
+                    // sake of additional styling
+                    appClass : 'guac-input-color-picker',
+
+                    // 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 : $scope.palette || [],
+
+                    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
+                        }
+
+                    },
+
+                    // Use translation strings for buttons
+                    strings : strings
+
+                });
+
+                // Hide color picker after user clicks "cancel"
+                pickr.on('cancel', function colorChangeCanceled() {
+                    pickr.hide();
+                });
+
+                // Keep model in sync with changes to the color picker
+                pickr.on('save', function colorChanged(color) {
+                    $scope.$evalAsync(function updateModel() {
+                        $scope.model = color.toHEXA().toString();
+                    });
+                });
+
+                // Keep color picker in sync with changes to the model
+                pickr.on('init', function pickrReady(color) {
+                    $scope.$watch('model', function modelChanged(model) {
+                        pickr.setColor(model);
+                    });
+                });
+
+            }
+
+            // If the "Pickr" color picker cannot be loaded (Internet Explorer),
+            // let the scope above us know
+            catch (e) {
+                $scope.colorPickerUnavailable = true;
+                $scope.$emit('guacInputColorUnavailable', e);
+            }
+
+        }, 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/formService.js b/guacamole/src/main/webapp/app/form/services/formService.js
index 168a1ef..24c29ac 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'
         }
 
     };
@@ -206,6 +234,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.
@@ -213,6 +246,10 @@
          * model:
          *     The current String value of the field, if any.
          *
+         * disabled:
+         *     A boolean value which is true if the field should be disabled.
+         *     If false or undefined, the field should be enabled.
+         *
          * @param {Element} fieldContainer
          *     The DOM Element whose contents should be replaced with the
          *     compiled field template.
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..e906f7d 100644
--- a/guacamole/src/main/webapp/app/form/templates/checkboxField.html
+++ b/guacamole/src/main/webapp/app/form/templates/checkboxField.html
@@ -1 +1,7 @@
-<input type="checkbox" ng-model="typedValue" autocorrect="off" autocapitalize="off"/>
\ No newline at end of file
+<input type="checkbox"
+       ng-attr-id="{{ fieldId }}"
+       ng-disabled="disabled"
+       ng-model="typedValue"
+       guac-focus="focused"
+       autocorrect="off"
+       autocapitalize="off"/>
diff --git a/guacamole/src/main/webapp/app/form/templates/dateField.html b/guacamole/src/main/webapp/app/form/templates/dateField.html
index a186e19..7673e36 100644
--- a/guacamole/src/main/webapp/app/form/templates/dateField.html
+++ b/guacamole/src/main/webapp/app/form/templates/dateField.html
@@ -1,8 +1,11 @@
 <div class="date-field">
     <input type="date"
+           ng-disabled="disabled"
+           ng-attr-id="{{ fieldId }}"
            ng-model="typedValue"
            ng-model-options="modelOptions"
            guac-lenient-date
+           guac-focus="focused"
            placeholder="{{'FORM.FIELD_PLACEHOLDER_DATE' | translate}}"
            autocorrect="off"
            autocapitalize="off"/>
diff --git a/guacamole/src/main/webapp/app/form/templates/emailField.html b/guacamole/src/main/webapp/app/form/templates/emailField.html
index db6d3be..cbfbb90 100644
--- a/guacamole/src/main/webapp/app/form/templates/emailField.html
+++ b/guacamole/src/main/webapp/app/form/templates/emailField.html
@@ -1,8 +1,11 @@
 <div class="email-field">
     <input type="email"
+           ng-disabled="disabled"
+           ng-attr-id="{{ fieldId }}"
            ng-model="model"
            ng-hide="readOnly"
+           guac-focus="focused"
            autocorrect="off"
            autocapitalize="off"/>
     <a href="mailto:{{model}}" ng-show="readOnly">{{model}}</a>
-</div>
\ No newline at end of file
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/form.html b/guacamole/src/main/webapp/app/form/templates/form.html
index b58393f..6b19bcc 100644
--- a/guacamole/src/main/webapp/app/form/templates/form.html
+++ b/guacamole/src/main/webapp/app/form/templates/form.html
@@ -9,7 +9,10 @@
         <div class="fields">
             <guac-form-field ng-repeat="field in form.fields" namespace="namespace"
                              ng-if="isVisible(field)"
-                             field="field" model="values[field.name]"></guac-form-field>
+                             data-disabled="disabled"
+                             focused="isFocused(field)"
+                             field="field"
+                             model="values[field.name]"></guac-form-field>
         </div>
 
     </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..fc6e675
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/templates/guacInputColor.html
@@ -0,0 +1,10 @@
+<div class="guac-input-color"
+     ng-class="{
+         'dark' : isDark(),
+         'read-only' : colorPickerUnavailable
+     }"
+     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..2a22ff2
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/templates/languageField.html
@@ -0,0 +1,4 @@
+<select guac-focus="focused"
+        ng-attr-id="{{ fieldId }}"
+        ng-model="model"
+        ng-options="language.key as language.value for language in languages | toArray | orderBy: key"></select>
diff --git a/guacamole/src/main/webapp/app/form/templates/numberField.html b/guacamole/src/main/webapp/app/form/templates/numberField.html
index 3d6312e..c86fb8e 100644
--- a/guacamole/src/main/webapp/app/form/templates/numberField.html
+++ b/guacamole/src/main/webapp/app/form/templates/numberField.html
@@ -1 +1,7 @@
-<input type="number" ng-model="typedValue" autocorrect="off" autocapitalize="off"/>
\ No newline at end of file
+<input type="number"
+       ng-disabled="disabled"
+       ng-attr-id="{{ fieldId }}"
+       ng-model="typedValue"
+       guac-focus="focused"
+       autocorrect="off"
+       autocapitalize="off"/>
diff --git a/guacamole/src/main/webapp/app/form/templates/passwordField.html b/guacamole/src/main/webapp/app/form/templates/passwordField.html
index 506d8b6..35eba9e 100644
--- a/guacamole/src/main/webapp/app/form/templates/passwordField.html
+++ b/guacamole/src/main/webapp/app/form/templates/passwordField.html
@@ -1,4 +1,11 @@
 <div class="password-field">
-    <input type="{{passwordInputType}}" ng-model="model" ng-trim="false" autocorrect="off" autocapitalize="off"/>
+    <input type="{{passwordInputType}}"
+           ng-disabled="disabled"
+           ng-attr-id="{{ fieldId }}"
+           ng-model="model"
+           ng-trim="false"
+           guac-focus="focused"
+           autocorrect="off"
+           autocapitalize="off"/>
     <div class="icon toggle-password" ng-click="togglePassword()" title="{{getTogglePasswordHelpText() | translate}}"></div>
-</div>
\ No newline at end of file
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/selectField.html b/guacamole/src/main/webapp/app/form/templates/selectField.html
index 3bd2bb8..2c672aa 100644
--- a/guacamole/src/main/webapp/app/form/templates/selectField.html
+++ b/guacamole/src/main/webapp/app/form/templates/selectField.html
@@ -1 +1,5 @@
-<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-disabled="disabled"
+        guac-focus="focused"
+        ng-model="model"
+        ng-options="option as getFieldOption(option) | translate for option in field.options | orderBy: value"></select>
diff --git a/guacamole/src/main/webapp/app/form/templates/terminalColorSchemeField.html b/guacamole/src/main/webapp/app/form/templates/terminalColorSchemeField.html
new file mode 100644
index 0000000..a8425e4
--- /dev/null
+++ b/guacamole/src/main/webapp/app/form/templates/terminalColorSchemeField.html
@@ -0,0 +1,63 @@
+<div class="terminal-color-scheme-field" ng-class="{
+        'custom-color-scheme-details-visible' : detailsShown,
+        'custom-color-scheme-details-hidden' : !detailsShown
+    }">
+
+    <!-- Pre-defined color scheme options -->
+    <select ng-attr-id="{{ fieldId }}" ng-model="selectedColorScheme">
+        <option ng-repeat="option in field.options | orderBy: value"
+                ng-value="option">{{ getFieldOption(option) | translate }}</option>
+        <option value="custom">{{ 'COLOR_SCHEME.FIELD_OPTION_CUSTOM' | translate }}</option>
+    </select>
+
+    <!-- Custom color scheme -->
+    <div class="custom-color-scheme" ng-show="isCustom()">
+
+        <!-- Default foreground color -->
+        <div class="custom-color-scheme-section default-color foreground">
+            <guac-input-color model="customColorScheme.foreground"
+                              palette="defaultPalette">
+                {{ 'COLOR_SCHEME.FIELD_HEADER_FOREGROUND' | translate }}
+            </guac-input-color>
+        </div>
+
+        <!-- Default background color -->
+        <div class="custom-color-scheme-section default-color background">
+            <guac-input-color model="customColorScheme.background"
+                              palette="defaultPalette">
+                {{ 'COLOR_SCHEME.FIELD_HEADER_BACKGROUND' | translate }}
+            </guac-input-color>
+        </div>
+
+        <!-- Low intensity portion of 16-color palette -->
+        <div class="custom-color-scheme-section palette low-intensity">
+            <guac-input-color ng-repeat="index in lowIntensity"
+                              model="customColorScheme.colors[index]"
+                              palette="defaultPalette">
+                {{ index }}
+            </guac-input-color>
+        </div>
+
+        <!-- High intensity portion of 16-color palette -->
+        <div class="custom-color-scheme-section palette high-intensity">
+            <guac-input-color ng-repeat="index in highIntensity"
+                              model="customColorScheme.colors[index]"
+                              palette="defaultPalette">
+                {{ index }}
+            </guac-input-color>
+        </div>
+
+    </div>
+
+    <!-- Show/hide details -->
+    <h4 class="custom-color-scheme-details-header" ng-show="isCustom()">
+        {{'COLOR_SCHEME.SECTION_HEADER_DETAILS' | translate}}
+        <a class="custom-color-scheme-show-details" ng-click="showDetails()">{{'COLOR_SCHEME.ACTION_SHOW_DETAILS' | translate}}</a>
+        <a class="custom-color-scheme-hide-details" ng-click="hideDetails()">{{'COLOR_SCHEME.ACTION_HIDE_DETAILS' | translate}}</a>
+    </h4>
+
+    <!-- Custom color scheme details (internal representation -->
+    <textarea class="custom-color-scheme-details" spellcheck="false"
+              ng-model="model" ng-show="isCustom()"></textarea>
+
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/textAreaField.html b/guacamole/src/main/webapp/app/form/templates/textAreaField.html
index 082476f..9761af7 100644
--- a/guacamole/src/main/webapp/app/form/templates/textAreaField.html
+++ b/guacamole/src/main/webapp/app/form/templates/textAreaField.html
@@ -1 +1,6 @@
-<textarea ng-model="model" autocorrect="off" autocapitalize="off"></textarea>
\ No newline at end of file
+<textarea ng-attr-id="{{ fieldId }}"
+          ng-model="model"
+          ng-disabled="disabled"
+          guac-focus="focused"
+          autocorrect="off"
+          autocapitalize="off"></textarea>
diff --git a/guacamole/src/main/webapp/app/form/templates/textField.html b/guacamole/src/main/webapp/app/form/templates/textField.html
index a338db4..3aea2bc 100644
--- a/guacamole/src/main/webapp/app/form/templates/textField.html
+++ b/guacamole/src/main/webapp/app/form/templates/textField.html
@@ -1,7 +1,14 @@
 <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"
+           ng-disabled="disabled"
+           guac-focus="focused"
+           autocorrect="off"
+           autocapitalize="off"/>
+    <datalist ng-if="dataListId" ng-attr-id="{{ dataListId }}">
         <option ng-repeat="option in field.options | orderBy: option"
                 value="{{ option }}">{{ getFieldOption(option) | translate }}</option>
     </datalist>
-</div>
\ No newline at end of file
+</div>
diff --git a/guacamole/src/main/webapp/app/form/templates/timeField.html b/guacamole/src/main/webapp/app/form/templates/timeField.html
index 24ae968..2a88230 100644
--- a/guacamole/src/main/webapp/app/form/templates/timeField.html
+++ b/guacamole/src/main/webapp/app/form/templates/timeField.html
@@ -1,7 +1,10 @@
 <div class="time-field">
     <input type="time"
+           ng-disabled="disabled"
+           ng-attr-id="{{ fieldId }}"
            ng-model="typedValue"
            ng-model-options="modelOptions"
+           guac-focus="focused"
            guac-lenient-time
            placeholder="{{'FORM.FIELD_PLACEHOLDER_TIME' | translate}}"
            autocorrect="off"
diff --git a/guacamole/src/main/webapp/app/form/templates/timeZoneField.html b/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
index 15fd4d6..ecab57d 100644
--- a/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
+++ b/guacamole/src/main/webapp/app/form/templates/timeZoneField.html
@@ -2,12 +2,15 @@
 
     <!-- Available time zone regions -->
     <select class="time-zone-region"
+            ng-disabled="disabled"
+            guac-focus="focused"
+            ng-attr-id="{{ fieldId }}"
             ng-model="region"
             ng-options="name for name in regions | orderBy: name"></select>
 
     <!-- Time zones within selected region -->
     <select class="time-zone"
-            ng-disabled="!region"
+            ng-disabled="disabled || !region"
             ng-model="model"
             ng-options="timeZone.value as timeZone.key for timeZone in timeZones[region] | toArray | orderBy: key"></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/home/templates/connectionGroup.html b/guacamole/src/main/webapp/app/home/templates/connectionGroup.html
index 6b54264..909aacf 100644
--- a/guacamole/src/main/webapp/app/home/templates/connectionGroup.html
+++ b/guacamole/src/main/webapp/app/home/templates/connectionGroup.html
@@ -1,4 +1,4 @@
 <span class="home-connection-group name">
-    <a ng-show="item.balancing" ng-href="#/client/{{ getClientIdentifier() }}">{{item.name}}</a>
+    <a ng-show="item.balancing" ng-href="#/client/{{ item.getClientIdentifier() }}">{{item.name}}</a>
     <span ng-show="!item.balancing">{{item.name}}</span>
 </span>
diff --git a/guacamole/src/main/webapp/app/index/config/indexHttpPatchConfig.js b/guacamole/src/main/webapp/app/index/config/httpDefaults.js
similarity index 66%
rename from guacamole/src/main/webapp/app/index/config/indexHttpPatchConfig.js
rename to guacamole/src/main/webapp/app/index/config/httpDefaults.js
index 114d598..9de1ce2 100644
--- a/guacamole/src/main/webapp/app/index/config/indexHttpPatchConfig.js
+++ b/guacamole/src/main/webapp/app/index/config/httpDefaults.js
@@ -18,14 +18,19 @@
  */
 
 /**
- * The config block for setting up the HTTP PATCH method.
+ * Defaults for the AngularJS $http service.
  */
-angular.module('index').config(['$httpProvider', 
-        function indexHttpPatchConfig($httpProvider) {
-    
+angular.module('index').config(['$httpProvider', function httpDefaults($httpProvider) {
+
+    // Do not cache the responses of GET requests
+    $httpProvider.defaults.headers.get = {
+        'Cache-Control' : 'no-cache',
+        'Pragma' : 'no-cache'
+    };
+
+    // Use "application/json" content type by default for PATCH requests
     $httpProvider.defaults.headers.patch = {
-        'Content-Type': 'application/json'
-    }
+        'Content-Type' : 'application/json'
+    };
+
 }]);
-
-
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/login/directives/login.js b/guacamole/src/main/webapp/app/login/directives/login.js
index 562e397..a414548 100644
--- a/guacamole/src/main/webapp/app/login/directives/login.js
+++ b/guacamole/src/main/webapp/app/login/directives/login.js
@@ -66,6 +66,7 @@
         var Field = $injector.get('Field');
 
         // Required services
+        var $rootScope            = $injector.get('$rootScope');
         var $route                = $injector.get('$route');
         var authenticationService = $injector.get('authenticationService');
         var requestService        = $injector.get('requestService');
@@ -92,6 +93,23 @@
         $scope.remainingFields = [];
 
         /**
+         * Whether an authentication attempt has been submitted. This will be
+         * set to true once credentials have been submitted and will only be
+         * reset to false once the attempt has been fully processed, including
+         * rerouting the user to the requested page if the attempt succeeded.
+         *
+         * @type Boolean
+         */
+        $scope.submitted = false;
+
+        /**
+         * The field that is most relevant to the user.
+         *
+         * @type Field
+         */
+        $scope.relevantField = null;
+
+        /**
          * Returns whether a previous login attempt is continuing.
          *
          * @return {Boolean}
@@ -133,6 +151,8 @@
                     $scope.enteredValues[field.name] = '';
             });
 
+            $scope.relevantField = getRelevantField();
+
         });
 
         /**
@@ -141,21 +161,27 @@
          */
         $scope.login = function login() {
 
+            // Authentication is now in progress
+            $scope.submitted = true;
+
             // Start with cleared status
-            $scope.loginError  = null;
+            $scope.loginError = null;
 
             // Attempt login once existing session is destroyed
             authenticationService.authenticate($scope.enteredValues)
 
-            // Clear and reload upon success
+            // Retry route upon success (entered values will be cleared only
+            // after route change has succeeded as this can take time)
             .then(function loginSuccessful() {
-                $scope.enteredValues = {};
                 $route.reload();
             })
 
             // Reset upon failure
             ['catch'](requestService.createErrorCallback(function loginFailed(error) {
 
+                // Initial submission is complete and has failed
+                $scope.submitted = false;
+
                 // Clear out passwords if the credentials were rejected for any reason
                 if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) {
 
@@ -183,6 +209,32 @@
 
         };
 
+        /**
+         * Returns the field most relevant to the user given the current state
+         * of the login process. This will normally be the first empty field.
+         *
+         * @return {Field}
+         *     The field most relevant, null if there is no single most relevant
+         *     field.
+         */
+        var getRelevantField = function getRelevantField() {
+
+            for (var i = 0; i < $scope.remainingFields.length; i++) {
+                var field = $scope.remainingFields[i];
+                if (!$scope.enteredValues[field.name])
+                    return field;
+            }
+
+            return null;
+
+        };
+
+        // Reset state after authentication and routing have succeeded
+        $rootScope.$on('$routeChangeSuccess', function routeChanged() {
+            $scope.enteredValues = {};
+            $scope.submitted = false;
+        });
+
     }];
 
     return directive;
diff --git a/guacamole/src/main/webapp/app/login/styles/dialog.css b/guacamole/src/main/webapp/app/login/styles/dialog.css
index c9c6a4a..e833533 100644
--- a/guacamole/src/main/webapp/app/login/styles/dialog.css
+++ b/guacamole/src/main/webapp/app/login/styles/dialog.css
@@ -17,27 +17,20 @@
  * under the License.
  */
 
-.login-ui.error .login-dialog {
-    animation-name: shake-head;
-    animation-duration: 0.25s;
-    animation-timing-function: linear;
-    -webkit-animation-name: shake-head;
-    -webkit-animation-duration: 0.25s;
-    -webkit-animation-timing-function: linear;
+.login-ui {
+    animation: fadein 0.125s linear;
+    -moz-animation: fadein 0.125s linear;
+    -webkit-animation: fadein 0.125s linear;
 }
 
-.login-ui div.login-dialog-middle {
+.login-ui .login-dialog-middle {
     width: 100%;
     display: table-cell;
     vertical-align: middle;
     text-align: center;
 }
 
-.login-ui div.login-dialog {
-
-    animation: fadein 0.125s linear;
-    -moz-animation: fadein 0.125s linear;
-    -webkit-animation: fadein 0.125s linear;
+.login-ui .login-dialog {
 
     width: 100%;
     max-width: 3in;
@@ -115,7 +108,7 @@
     background-image: url("images/guac-tricolor.png");
 }
 
-.login-ui.continuation div.login-dialog {
+.login-ui.continuation .login-dialog {
     border-right: none;
     border-left: none;
     box-shadow: none;
@@ -126,3 +119,12 @@
 .login-ui.continuation .login-dialog .version {
     display: none;
 }
+
+.login-ui.error .login-dialog {
+    animation-name: shake-head;
+    animation-duration: 0.25s;
+    animation-timing-function: linear;
+    -webkit-animation-name: shake-head;
+    -webkit-animation-duration: 0.25s;
+    -webkit-animation-timing-function: linear;
+}
diff --git a/guacamole/src/main/webapp/app/login/templates/login.html b/guacamole/src/main/webapp/app/login/templates/login.html
index 26a3f18..04111ed 100644
--- a/guacamole/src/main/webapp/app/login/templates/login.html
+++ b/guacamole/src/main/webapp/app/login/templates/login.html
@@ -23,13 +23,25 @@
 
                 <!-- Login fields -->
                 <div class="login-fields">
-                    <guac-form namespace="'LOGIN'" content="remainingFields" model="enteredValues"></guac-form>
+                    <guac-form
+                        namespace="'LOGIN'"
+                        content="remainingFields"
+                        model="enteredValues"
+                        focused="relevantField.name"
+                        data-disabled="submitted"></guac-form>
                 </div>
 
-                <!-- Submit button -->
+                <!-- Login/continue button -->
                 <div class="buttons">
-                    <input type="submit" name="login" class="login" value="{{'LOGIN.ACTION_LOGIN' | translate}}"/>
-                    <input type="submit" name="login" class="continue-login" value="{{'LOGIN.ACTION_CONTINUE' | translate}}"/>
+
+                    <input type="submit" name="login" class="login"
+                           ng-disabled="submitted"
+                           value="{{'LOGIN.ACTION_LOGIN' | translate}}"/>
+
+                    <input type="submit" name="login" class="continue-login"
+                           ng-disabled="submitted"
+                           value="{{'LOGIN.ACTION_CONTINUE' | translate}}"/>
+
                 </div>
 
             </form>
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/directives/guacMenu.js b/guacamole/src/main/webapp/app/navigation/directives/guacMenu.js
index 92e2c6c..230e902 100644
--- a/guacamole/src/main/webapp/app/navigation/directives/guacMenu.js
+++ b/guacamole/src/main/webapp/app/navigation/directives/guacMenu.js
@@ -34,7 +34,16 @@
              *
              * @type String
              */
-            menuTitle : '='
+            menuTitle : '=',
+
+            /**
+             * Whether the menu should remain open while the user interacts
+             * with the contents of the menu. By default, the menu will close
+             * if the user clicks within the menu contents.
+             *
+             * @type Boolean
+             */
+            interactive : '='
 
         },
 
@@ -81,21 +90,19 @@
                 $scope.menuShown = !$scope.menuShown;
             };
 
-            // Close menu when use clicks anywhere else
-            document.body.addEventListener('click', function clickOutsideMenu() {
+            // Close menu when user clicks anywhere outside this specific menu
+            document.body.addEventListener('click', function clickOutsideMenu(e) {
                 $scope.$apply(function closeMenu() {
-                    $scope.menuShown = false;
+                    if (e.target !== element && !element.contains(e.target))
+                        $scope.menuShown = false;
                 });
             }, false);
 
-            // Prevent click within menu from triggering the outside-menu handler
-            element.addEventListener('click', function clickInsideMenu(e) {
-                e.stopPropagation();
-            }, false);
-
-            // Prevent click within menu contents from toggling menu visibility
+            // Prevent clicks within menu contents from toggling menu visibility
+            // if the menu contents are intended to be interactive
             contents.addEventListener('click', function clickInsideMenuContents(e) {
-                e.stopPropagation();
+                if ($scope.interactive)
+                    e.stopPropagation();
             }, false);
 
         }] // end controller
diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js
index 2af426e..f91303a 100644
--- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js
+++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js
@@ -67,7 +67,6 @@
      */
     var generateHomePage = function generateHomePage(rootGroups, permissions) {
 
-        var homePage = null;
         var settingsPages = generateSettingsPages(permissions);
 
         // If user has access to settings pages, return home page and skip
@@ -79,67 +78,87 @@
         if (settingsPages.length > 2)
             return SYSTEM_HOME_PAGE;
 
+        // If exactly one connection or balancing group is available, use
+        // that as the home page
+        var clientPages = service.getClientPages(rootGroups);
+        return (clientPages.length === 1) ? clientPages[0] : SYSTEM_HOME_PAGE;
+
+    };
+
+    /**
+     * Adds to the given array all pages that the current user may use to
+     * access connections or balancing groups that are descendants of the given
+     * connection group.
+     *
+     * @param {PageDefinition[]} clientPages
+     *     The array that pages should be added to.
+     *
+     * @param {String} dataSource
+     *     The data source containing the given connection group.
+     *
+     * @param {ConnectionGroup} connectionGroup
+     *     The connection group ancestor of the connection or balancing group
+     *     descendants whose pages should be added to the given array.
+     */
+    var addClientPages = function addClientPages(clientPages, dataSource, connectionGroup) {
+
+        // Add pages for all child connections
+        angular.forEach(connectionGroup.childConnections, function addConnectionPage(connection) {
+            clientPages.push(new PageDefinition({
+                name : connection.name,
+                url  : '/client/' + ClientIdentifier.toString({
+                    dataSource : dataSource,
+                    type       : ClientIdentifier.Types.CONNECTION,
+                    id         : connection.identifier
+                })
+            }));
+        });
+
+        // Add pages for all child balancing groups, as well as the connectable
+        // descendants of all balancing groups of any type
+        angular.forEach(connectionGroup.childConnectionGroups, function addConnectionGroupPage(connectionGroup) {
+
+            if (connectionGroup.type === ConnectionGroup.Type.BALANCING) {
+                clientPages.push(new PageDefinition({
+                    name : connectionGroup.name,
+                    url  : '/client/' + ClientIdentifier.toString({
+                        dataSource : dataSource,
+                        type       : ClientIdentifier.Types.CONNECTION_GROUP,
+                        id         : connectionGroup.identifier
+                    })
+                }));
+            }
+
+            addClientPages(clientPages, dataSource, connectionGroup);
+
+        });
+
+    };
+
+    /**
+     * Returns a full list of all pages that the current user may use to access
+     * a connection or balancing group, regardless of the depth of those
+     * connections/groups within the connection hierarchy.
+     *
+     * @param {Object.<String, ConnectionGroup>} rootGroups
+     *     A map of all root connection groups visible to the current user,
+     *     where each key is the identifier of the corresponding data source.
+     *
+     * @returns {PageDefinition[]}
+     *     A list of all pages that the current user may use to access a
+     *     connection or balancing group.
+     */
+    service.getClientPages = function getClientPages(rootGroups) {
+
+        var clientPages = [];
+
         // Determine whether a connection or balancing group should serve as
         // the home page
         for (var dataSource in rootGroups) {
+            addClientPages(clientPages, dataSource, rootGroups[dataSource]);
+        }
 
-            // Get corresponding root group
-            var rootGroup = rootGroups[dataSource];
-
-            // Get children
-            var connections      = rootGroup.childConnections      || [];
-            var connectionGroups = rootGroup.childConnectionGroups || [];
-
-            // Calculate total number of root-level objects
-            var totalRootObjects = connections.length + connectionGroups.length;
-
-            // If exactly one connection or balancing group is available, use
-            // that as the home page
-            if (homePage === null && totalRootObjects === 1) {
-
-                var connection      = connections[0];
-                var connectionGroup = connectionGroups[0];
-
-                // Only one connection present, use as home page
-                if (connection) {
-                    homePage = new PageDefinition({
-                        name : connection.name,
-                        url  : '/client/' + ClientIdentifier.toString({
-                            dataSource : dataSource,
-                            type       : ClientIdentifier.Types.CONNECTION,
-                            id         : connection.identifier
-                        })
-                    });
-                }
-
-                // Only one balancing group present, use as home page
-                if (connectionGroup
-                        && connectionGroup.type === ConnectionGroup.Type.BALANCING
-                        && _.isEmpty(connectionGroup.childConnections)
-                        && _.isEmpty(connectionGroup.childConnectionGroups)) {
-                    homePage = new PageDefinition({
-                        name : connectionGroup.name,
-                        url  : '/client/' + ClientIdentifier.toString({
-                            dataSource : dataSource,
-                            type       : ClientIdentifier.Types.CONNECTION_GROUP,
-                            id         : connectionGroup.identifier
-                        })
-                    });
-                }
-
-            }
-
-            // Otherwise, a connection or balancing group cannot serve as the
-            // home page
-            else if (totalRootObjects >= 1) {
-                homePage = null;
-                break;
-            }
-
-        } // end for each data source
-
-        // Use default home page if no other is available
-        return homePage || SYSTEM_HOME_PAGE;
+        return clientPages;
 
     };
 
diff --git a/guacamole/src/main/webapp/app/navigation/styles/menu.css b/guacamole/src/main/webapp/app/navigation/styles/menu.css
index 1e4e75e..65e010b 100644
--- a/guacamole/src/main/webapp/app/navigation/styles/menu.css
+++ b/guacamole/src/main/webapp/app/navigation/styles/menu.css
@@ -68,6 +68,11 @@
     padding: 0.5em;
     padding-right: 2em;
 
+    white-space: nowrap;
+    overflow: hidden;
+    width: 100%;
+    text-overflow: ellipsis;
+
     -ms-flex: 0 0 auto;
     -moz-box-flex: 0;
     -webkit-box-flex: 0;
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/cz.json b/guacamole/src/main/webapp/translations/cz.json
new file mode 100644
index 0000000..be916dd
--- /dev/null
+++ b/guacamole/src/main/webapp/translations/cz.json
@@ -0,0 +1,882 @@
+{
+
+    "NAME" : "Čeština",
+
+    "APP" : {
+
+        "ACTION_ACKNOWLEDGE"        : "OK",
+        "ACTION_CANCEL"             : "Zrušit",
+        "ACTION_CLONE"              : "Klonovat",
+        "ACTION_CONTINUE"           : "Pokračovat",
+        "ACTION_DELETE"             : "Smazat",
+        "ACTION_DELETE_SESSIONS"    : "Ukončit sezení",
+        "ACTION_DOWNLOAD"           : "Stáhnout",
+        "ACTION_LOGIN"              : "Přihlásit",
+        "ACTION_LOGOUT"             : "Odhlásit",
+        "ACTION_MANAGE_CONNECTIONS" : "Připojení",
+        "ACTION_MANAGE_PREFERENCES" : "Vlastnosti",
+        "ACTION_MANAGE_SETTINGS"    : "Nastavení",
+        "ACTION_MANAGE_SESSIONS"    : "Aktivní sezení",
+        "ACTION_MANAGE_USERS"       : "Uživatelé",
+        "ACTION_MANAGE_USER_GROUPS" : "Skupiny",
+        "ACTION_NAVIGATE_BACK"      : "Zpět",
+        "ACTION_NAVIGATE_HOME"      : "Domů",
+        "ACTION_SAVE"               : "Uložit",
+        "ACTION_SEARCH"             : "Hledat",
+        "ACTION_SHARE"              : "Sdílet",
+        "ACTION_UPDATE_PASSWORD"    : "Změnit heslo",
+        "ACTION_VIEW_HISTORY"       : "Historie",
+
+        "DIALOG_HEADER_ERROR" : "Chyba",
+
+        "ERROR_PAGE_UNAVAILABLE"  : "Došlo k chybě a tuto akci nelze dokončit. Pokud problém přetrvává, informujte prosím správce systému nebo zkontrolujte systémové protokoly.",
+        "ERROR_PASSWORD_BLANK"    : "Heslo nesmí být prázdné.",
+        "ERROR_PASSWORD_MISMATCH" : "Hesla nesouhlasí.",
+
+        "FIELD_HEADER_PASSWORD"       : "Heslo:",
+        "FIELD_HEADER_PASSWORD_AGAIN" : "Heslo znovu:",
+
+        "FIELD_PLACEHOLDER_FILTER"    : "Filtr",
+
+        "FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss",
+
+        "INFO_ACTIVE_USER_COUNT" : "V současné době používá {USERS} {USERS, plural, one{user} other{users}}.",
+
+        "TEXT_ANONYMOUS_USER"   : "Anonym",
+        "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{{VALUE, plural, one{second} other{seconds}}} minute{{VALUE, plural, one{minute} other{minutes}}} hour{{VALUE, plural, one{hour} other{hours}}} day{{VALUE, plural, one{day} other{days}}} other{}}"
+
+    },
+
+    "CLIENT" : {
+
+        "ACTION_ACKNOWLEDGE"               : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Vyčistit",
+        "ACTION_DISCONNECT"                : "Odpojit",
+        "ACTION_LOGOUT"                    : "@:APP.ACTION_LOGOUT",
+        "ACTION_NAVIGATE_BACK"             : "@:APP.ACTION_NAVIGATE_BACK",
+        "ACTION_NAVIGATE_HOME"             : "@:APP.ACTION_NAVIGATE_HOME",
+        "ACTION_RECONNECT"                 : "Znovu připojit",
+        "ACTION_SAVE_FILE"                 : "@:APP.ACTION_SAVE",
+        "ACTION_SHARE"                     : "@:APP.ACTION_SHARE",
+        "ACTION_UPLOAD_FILES"              : "Nahrát soubory",
+
+        "DIALOG_HEADER_CONNECTING"       : "Připojování",
+        "DIALOG_HEADER_CONNECTION_ERROR" : "Chyba připojení",
+        "DIALOG_HEADER_DISCONNECTED"     : "Odpojeno",
+
+        "ERROR_CLIENT_201"     : "Synchronizační server je zaneprázdněn, zkuste to prosím znovu později.",
+        "ERROR_CLIENT_202"     : "Guacamole server zavřel spojení protože vzdálený počítač příliš dlouho neodpovídal. Zkuste to prosím později, nebo kontaktujte správce.",
+        "ERROR_CLIENT_203"     : "Chyba vzdáleného serveru, spojení bylo uzavřeno. Zkuste to prosím později, nebo kontaktujte správce.",
+        "ERROR_CLIENT_207"     : "Server vzdálené plochy je aktuálně nedostupný. Pokud problém přetrvává, informujte prosím správce systému nebo zkontrolujte systémové protokoly.",
+        "ERROR_CLIENT_208"     : "Server vzdálené plochy není aktuálně k dispozici. Pokud problém přetrvává, informujte prosím správce systému nebo zkontrolujte systémové protokoly.",
+        "ERROR_CLIENT_209"     : "Server vzdálené plochy ukončil připojení, protože je v konfliktu s jiným připojením. Prosím zkuste to znovu později.",
+        "ERROR_CLIENT_20A"     : "Server vzdálené plochy ukončil připojení, protože se zdálo být neaktivní. Pokud je to nežádoucí nebo neočekávané, informujte prosím správce systému nebo zkontrolujte nastavení systému.",
+        "ERROR_CLIENT_20B"     : "Server vzdálené plochy násilně uzavřel připojení. Pokud je to nežádoucí nebo neočekávané, informujte prosím správce systému nebo zkontrolujte systémové protokoly.",
+        "ERROR_CLIENT_301"     : "Přihlášení selhalo. Připojte se a zkuste to znovu.",
+        "ERROR_CLIENT_303"     : "Server vzdálené plochy odepřel přístup k tomuto připojení. Pokud potřebujete přístup, požádejte správce systému, aby vám umožnil přístup, nebo zkontrolujte nastavení systému.",
+        "ERROR_CLIENT_308"     : "Server Guacamole ukončil spojení, protože z vašeho prohlížeče nebyla dostatečně dlouhá odezva, aby se zdálo, že je stále připojen. To je obvykle způsobeno problémy se sítí, jako je například nekvalitní bezdrátový signál, nebo jednoduše velmi pomalá síťová rychlost. Zkontrolujte síť a zkuste to znovu.",
+        "ERROR_CLIENT_31D"     : "Server Guacamole odepřel přístup k tomuto připojení, protože jste vyčerpali limit pro vícenásobné připojení tímto uživatelem. Zavřete jedno nebo více připojení a zkuste to znovu.",
+        "ERROR_CLIENT_DEFAULT" : "V rámci serveru Guacamole došlo k interní chybě a připojení bylo ukončeno. Pokud problém přetrvává, informujte prosím správce systému nebo zkontrolujte systémové protokoly.",
+
+        "ERROR_TUNNEL_201"     : "Server Guacamole odmítl tento pokus o připojení, protože existuje příliš mnoho aktivních připojení. Počkejte prosím několik minut a zkuste to znovu.",
+        "ERROR_TUNNEL_202"     : "Připojení bylo uzavřeno, protože serveru trvalo příliš dlouho, než odpověděl. To je obvykle způsobeno problémy se sítí, jako je například nekvalitní bezdrátový signál nebo pomalé připojení k síti. Zkontrolujte síťové připojení a zkuste to znovu nebo se obraťte na správce systému.",
+        "ERROR_TUNNEL_203"     : "Server zjistil chybu a připojení ukončil. Zkuste to prosím znovu nebo se obraťte na správce systému.",
+        "ERROR_TUNNEL_204"     : "Požadované připojení neexistuje. Zkontrolujte název připojení a zkuste to znovu.",
+        "ERROR_TUNNEL_205"     : "Toto připojení je právě používáno a vícenásobný přístup k tomuto připojení není povolen. Prosím zkuste to znovu později.",
+        "ERROR_TUNNEL_207"     : "Server Guacamole není v současné době dostupný. Zkontrolujte síť a zkuste to znovu",
+        "ERROR_TUNNEL_208"     : "Server Guacamole nepřijímá připojení. Zkontrolujte síť a zkuste to znovu.",
+        "ERROR_TUNNEL_301"     : "Nemáte oprávnění k přístupu k tomuto připojení, protože nejste přihlášeni. Přihlaste se a zkuste to znovu.",
+        "ERROR_TUNNEL_303"     : "Nemáte oprávnění k přístupu k tomuto připojení. Pokud potřebujete přístup, požádejte správce systému, aby vás přidal do seznamu povolených uživatelů nebo zkontrolujte nastavení systému.",
+        "ERROR_TUNNEL_308"     : "Server Guacamole ukončil spojení, protože z vašeho prohlížeče nebyla dostatečně dlouhá odezva, aby se zdálo, že je stále připojen. To je obvykle způsobeno problémy se sítí, jako je například nekvalitní bezdrátový signál, nebo jednoduše velmi pomalá síťová rychlost. Zkontrolujte síť a zkuste to znovu.",
+        "ERROR_TUNNEL_31D"     : "Server Guacamole odepřel přístup k tomuto připojení, protože jste vyčerpali limit pro vícenásobné připojení tímto uživatelem. Zavřete jedno nebo více připojení a zkuste to znovu.",
+        "ERROR_TUNNEL_DEFAULT" : "V rámci serveru Guacamole došlo k interní chybě a připojení bylo ukončeno. Pokud problém přetrvává, informujte prosím správce systému nebo zkontrolujte systémové protokoly.",
+
+        "ERROR_UPLOAD_100"     : "Přenos souborů není podporován nebo není povolen. Obraťte se na správce systému nebo zkontrolujte systémové protokoly.",
+        "ERROR_UPLOAD_201"     : "V současné době se přenáší příliš mnoho souborů. Počkejte prosím na dokončení probíhajících přenosů a akci opakujte.",
+        "ERROR_UPLOAD_202"     : "Soubor nelze přenést, protože serveru vzdálené plochy trvá příliš dlouho, než odpoví. Zkuste to prosím znovu nebo se obraťte na správce systému.",
+        "ERROR_UPLOAD_203"     : "Server vzdálené plochy zaznamenal chybu během přenosu. Zkuste to prosím znovu nebo se obraťte na správce systému.",
+        "ERROR_UPLOAD_204"     : "Cíl pro přenos souborů neexistuje. Zkontrolujte, zda cíl existuje a zkuste to znovu.",
+        "ERROR_UPLOAD_205"     : "Cíl přenosu souborů je aktuálně uzamčen. Počkejte prosím na dokončení probíhajících úkolů a zkuste to znovu.",
+        "ERROR_UPLOAD_301"     : "Nemáte oprávnění nahrát tento soubor, protože nejste přihlášeni. Přihlaste se a zkuste to znovu.",
+        "ERROR_UPLOAD_303"     : "Nemáte oprávnění k nahrání tohoto souboru. Pokud potřebujete přístup, zkontrolujte nastavení systému nebo se obraťte na správce systému.",
+        "ERROR_UPLOAD_308"     : "Přenos souboru se zastavil. To je obvykle způsobeno problémy se sítí, jako je například nekvalitní bezdrátový signál, nebo jednoduše velmi pomalé připojení k síťi. Zkontrolujte síť a zkuste to znovu.",
+        "ERROR_UPLOAD_31D"     : "V současné době se přenáší příliš mnoho souborů. Počkejte prosím na dokončení probíhajících přenosů a akci opakujte.",
+        "ERROR_UPLOAD_DEFAULT" : "V rámci serveru Guacamole došlo k interní chybě a připojení bylo ukončeno. Pokud problém přetrvává, informujte prosím správce systému nebo zkontrolujte systémové protokoly.",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_CLIPBOARD"           : "Zde se zobrazí text zkopírovaný / oříznutý v Guacamole. Změny níže uvedeného textu ovlivní vzdálenou schránku.",
+        "HELP_INPUT_METHOD_NONE"   : "Není použita žádná metoda vstupu. Vstup z klávesnice je přijímán z připojené fyzické klávesnice.",
+        "HELP_INPUT_METHOD_OSK"    : "Zobrazte a přijměte vstup z vestavěné klávesnice Guacamole na obrazovce. Klávesnice na obrazovce umožňuje zadávat kombinace kláves, které jinak mohou být nemožné (například Ctrl-Alt-Del).",
+        "HELP_INPUT_METHOD_TEXT"   : "Povolit psaní textu a emulovat události klávesnice na základě zadaného textu. To je nezbytné pro zařízení, jako jsou mobilní telefony, které nemají fyzickou klávesnici.",
+        "HELP_MOUSE_MODE"          : "Určuje, jak se bude vzdálená myš chovat s ohledem na dotyky.",
+        "HELP_MOUSE_MODE_ABSOLUTE" : "Tap to click. Kliknutí nastane v místě dotyku.",
+        "HELP_MOUSE_MODE_RELATIVE" : "Přetažením myši posuňte ukazatel myši a klepněte na tlačítko. Kliknutí nastane v místě ukazatele.",
+        "HELP_SHARE_LINK"          : "Aktuální připojení je sdíleno a může k němu přistupovat kdokoli s následujícím {LINKS, plural, one{link} other{links}}:",
+
+        "INFO_CONNECTION_SHARED" : "Toto připojení je nyní sdíleno.",
+        "INFO_NO_FILE_TRANSFERS" : "Žádné přenosy souborů.",
+
+        "NAME_INPUT_METHOD_NONE"   : "Žádné",
+        "NAME_INPUT_METHOD_OSK"    : "Na obrazovce, klávesnice",
+        "NAME_INPUT_METHOD_TEXT"   : "Textový vstup",
+        "NAME_KEY_CTRL"            : "Ctrl",
+        "NAME_KEY_ALT"             : "Alt",
+        "NAME_KEY_ESC"             : "Esc",
+        "NAME_KEY_TAB"             : "Tab",
+        "NAME_MOUSE_MODE_ABSOLUTE" : "Dotyková obrazovka",
+        "NAME_MOUSE_MODE_RELATIVE" : "Touchpad",
+
+        "SECTION_HEADER_CLIPBOARD"      : "Schránka",
+        "SECTION_HEADER_DEVICES"        : "Zařízení",
+        "SECTION_HEADER_DISPLAY"        : "Zobrazení",
+        "SECTION_HEADER_FILE_TRANSFERS" : "Přenos souborů",
+        "SECTION_HEADER_INPUT_METHOD"   : "Metoda vstupu",
+        "SECTION_HEADER_MOUSE_MODE"     : "Mód emulace myši",
+
+        "TEXT_ZOOM_AUTO_FIT"              : "Automaticky přizpůsobit prohlížeč oknu",
+        "TEXT_CLIENT_STATUS_IDLE"         : "Nečinný",
+        "TEXT_CLIENT_STATUS_CONNECTING"   : "Připojuji ke Guacamole...",
+        "TEXT_CLIENT_STATUS_DISCONNECTED" : "Byl jste odpojen.",
+        "TEXT_CLIENT_STATUS_UNSTABLE"     : "Síťové spojení ke  Guacamole serveru se zdá nestabilní.",
+        "TEXT_CLIENT_STATUS_WAITING"      : "Připojen ke Guacamole. Čekání na odpověď...",
+        "TEXT_RECONNECT_COUNTDOWN"        : "Znovu připojuji  {REMAINING} {REMAINING, plural, one{second} other{seconds}}...",
+        "TEXT_FILE_TRANSFER_PROGRESS"     : "{PROGRESS} {UNIT, select, b{B} kb{KB} mb{MB} gb{GB} other{}}",
+
+        "URL_OSK_LAYOUT" : "layouts/en-us-qwerty.json"
+
+    },
+
+    "DATA_SOURCE_DEFAULT" : {
+        "NAME" : "Výchozí (XML)"
+    },
+
+    "FORM" : {
+
+        "FIELD_PLACEHOLDER_DATE" : "YYYY-MM-dd",
+        "FIELD_PLACEHOLDER_TIME" : "HH:MM:SS",
+
+        "HELP_SHOW_PASSWORD" : "Klikněte pro zobrazní hesla",
+        "HELP_HIDE_PASSWORD" : "Klikněte pro skrytí hesla"
+
+    },
+
+    "HOME" : {
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT",
+
+        "INFO_NO_RECENT_CONNECTIONS" : "Žádná nedávná spojení.",
+
+        "PASSWORD_CHANGED" : "Heslo bylo změněno.",
+
+        "SECTION_HEADER_ALL_CONNECTIONS"    : "Všechna spojení",
+        "SECTION_HEADER_RECENT_CONNECTIONS" : "Nedávná spojení"
+
+    },
+
+    "LIST" : {
+
+        "TEXT_ANONYMOUS_USER" : "Anonymní"
+
+    },
+
+    "LOGIN" : {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CONTINUE"    : "@:APP.ACTION_CONTINUE",
+        "ACTION_LOGIN"       : "@:APP.ACTION_LOGIN",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "ERROR_INVALID_LOGIN" : "Neplatné přihlašovací jméno",
+
+        "FIELD_HEADER_USERNAME" : "Uživatelské jméno",
+        "FIELD_HEADER_PASSWORD" : "Heslo"
+
+    },
+
+    "MANAGE_CONNECTION" : {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"      : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"       : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"      : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"        : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Smazat spojení",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_LOCATION" : "Oblast:",
+        "FIELD_HEADER_NAME"     : "Jméno:",
+        "FIELD_HEADER_PROTOCOL" : "Protokol:",
+
+        "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "INFO_CONNECTION_DURATION_UNKNOWN" : "--",
+        "INFO_CONNECTION_ACTIVE_NOW"       : "Aktivní nyní",
+        "INFO_CONNECTION_NOT_USED"         : "Toto spojení ještě nebylo použito.",
+
+        "SECTION_HEADER_EDIT_CONNECTION" : "Upravit spojení",
+        "SECTION_HEADER_HISTORY"         : "Historie využítí",
+        "SECTION_HEADER_PARAMETERS"      : "Parametry",
+
+        "TABLE_HEADER_HISTORY_USERNAME"   : "Uživatelské jméno",
+        "TABLE_HEADER_HISTORY_START"      : "Čas začátku",
+        "TABLE_HEADER_HISTORY_DURATION"   : "Doba",
+        "TABLE_HEADER_HISTORY_REMOTEHOST" : "Vzdálený host",
+
+        "TEXT_CONFIRM_DELETE"   : "Spojení nemůže být obnoveno poté, co je smazáno. Opravdu chcete smazat toto spojení?",
+        "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION"
+
+    },
+
+    "MANAGE_CONNECTION_GROUP" : {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"      : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"       : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"      : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"        : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Smazat skupinu spojení",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_LOCATION" : "Lokace:",
+        "FIELD_HEADER_NAME"     : "Jméno:",
+        "FIELD_HEADER_TYPE"     : "Typ:",
+
+        "NAME_TYPE_BALANCING"      : "Vyvažování:",
+        "NAME_TYPE_ORGANIZATIONAL" : "Organizace:",
+
+        "SECTION_HEADER_EDIT_CONNECTION_GROUP" : "Upravit skupinu spojení",
+
+        "TEXT_CONFIRM_DELETE" : "Skupiny spojení nelze obnovit po jejich odstranění. Opravdu chcete odstranit tuto skupinu připojení?"
+
+    },
+
+    "MANAGE_SHARING_PROFILE" : {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"      : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"       : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"      : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"        : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Odstranit profil sdílení",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_NAME"               : "Jméno:",
+        "FIELD_HEADER_PRIMARY_CONNECTION" : "Primární spojení:",
+
+        "SECTION_HEADER_EDIT_SHARING_PROFILE" : "Upravit sdílený profil",
+        "SECTION_HEADER_PARAMETERS"           : "Parametry",
+
+        "TEXT_CONFIRM_DELETE" : "Po smazání nelze obnovit profily sdílení. Opravdu chcete smazat tento profil sdílení?"
+
+    },
+
+    "MANAGE_USER" : {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"      : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"       : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"      : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"        : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Smazat uživatele",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
+
+        "FIELD_HEADER_ADMINISTER_SYSTEM"            : "Spravovat systém:",
+        "FIELD_HEADER_CHANGE_OWN_PASSWORD"          : "Změnit vlastní heslo:",
+        "FIELD_HEADER_CREATE_NEW_USERS"             : "Vytvořit nové uživatele:",
+        "FIELD_HEADER_CREATE_NEW_USER_GROUPS"       : "Vytvořit novou uživatelskou skupinu:",
+        "FIELD_HEADER_CREATE_NEW_CONNECTIONS"       : "Vytvořit nové spojení:",
+        "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Vytvořit nové skupiny připojení:",
+        "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES"  : "Vytvořit nový sdílený profil:",
+        "FIELD_HEADER_PASSWORD"                     : "@:APP.FIELD_HEADER_PASSWORD",
+        "FIELD_HEADER_PASSWORD_AGAIN"               : "@:APP.FIELD_HEADER_PASSWORD_AGAIN",
+        "FIELD_HEADER_USERNAME"                     : "Uživatelské jméno:",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_NO_USER_GROUPS" : "Tento uživatel momentálně nepatří do žádné skupiny. Rozbalte tuto sekci, abyste mohli přidát skupiny.",
+
+        "INFO_READ_ONLY"                : "Omlouváme se, ale tento uživatelský účet nelze upravovat.",
+        "INFO_NO_USER_GROUPS_AVAILABLE" : "Nejsou k dispozici žádné skupiny.",
+
+        "SECTION_HEADER_ALL_CONNECTIONS"     : "Všechna připojení",
+        "SECTION_HEADER_CONNECTIONS"         : "Připojení",
+        "SECTION_HEADER_CURRENT_CONNECTIONS" : "Aktuální připojení",
+        "SECTION_HEADER_EDIT_USER"           : "Upravit uživatele",
+        "SECTION_HEADER_PERMISSIONS"         : "Oprávnění",
+        "SECTION_HEADER_USER_GROUPS"         : "Skupiny",
+
+        "TEXT_CONFIRM_DELETE" : "Po odstranění nelze uživatele obnovit. Opravdu chcete tohoto uživatele smazat?"
+
+    },
+
+    "MANAGE_USER_GROUP" : {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"      : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"       : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"      : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"        : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Smazat skupinu",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_ADMINISTER_SYSTEM"            : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM",
+        "FIELD_HEADER_CHANGE_OWN_PASSWORD"          : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD",
+        "FIELD_HEADER_CREATE_NEW_USERS"             : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS",
+        "FIELD_HEADER_CREATE_NEW_USER_GROUPS"       : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS",
+        "FIELD_HEADER_CREATE_NEW_CONNECTIONS"       : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS",
+        "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS",
+        "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES"  : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES",
+        "FIELD_HEADER_USER_GROUP_NAME"              : "Jméno skupiny:",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_NO_USER_GROUPS"        : "Tato skupina momentálně nepatří do žádné skupiny. Rozbalením této sekce ji přidáte do skupiny.",
+        "HELP_NO_MEMBER_USER_GROUPS" : "Tato skupina v současné době neobsahuje žádné skupiny. Rozbalením této sekce ji přidáte do skupiny.",
+        "HELP_NO_MEMBER_USERS"       : "Tato skupina v současné době neobsahuje žádné uživatele. Rozbalením této sekce přidáte uživatele.",
+
+        "INFO_READ_ONLY"                : "Je nám líto, ale tuto skupinu nelze upravovat.",
+        "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE",
+        "INFO_NO_USERS_AVAILABLE"       : "Žádní uživatelé nejsou k dispozici.",
+
+        "SECTION_HEADER_ALL_CONNECTIONS"     : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS",
+        "SECTION_HEADER_CONNECTIONS"         : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS",
+        "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS",
+        "SECTION_HEADER_EDIT_USER_GROUP"     : "Upravit skupinu",
+        "SECTION_HEADER_MEMBER_USERS"        : "Členský uživatel",
+        "SECTION_HEADER_MEMBER_USER_GROUPS"  : "Členské skupiny",
+        "SECTION_HEADER_PERMISSIONS"         : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS",
+        "SECTION_HEADER_USER_GROUPS"         : "Rodičovské skupiny",
+
+        "TEXT_CONFIRM_DELETE" : "Skupiny nelze obnovit po jejich odstranění. Opravdu chcete tuto skupinu smazat?"
+
+    },
+
+    "PROTOCOL_RDP" : {
+
+        "FIELD_HEADER_CLIENT_NAME"                : "Jméno klienta:",
+        "FIELD_HEADER_COLOR_DEPTH"                : "Barevná hloubka:",
+        "FIELD_HEADER_CONSOLE"                    : "Konzola pro správu:",
+        "FIELD_HEADER_CONSOLE_AUDIO"              : "Podpora zvuku v konzole:",
+        "FIELD_HEADER_CREATE_DRIVE_PATH"          : "Automaticky vytvořit disk:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH"      : "Automaticky vytvořit cestu k záznamu:",
+        "FIELD_HEADER_DISABLE_AUDIO"              : "Zakázat zvuk:",
+        "FIELD_HEADER_DISABLE_AUTH"               : "Zakázat ověřování:",
+        "FIELD_HEADER_DISABLE_COPY"               : "Zakázat kopírování ze vzdálené plochy:",
+        "FIELD_HEADER_DISABLE_PASTE"              : "Zakázat vkládání z klienta:",
+        "FIELD_HEADER_DOMAIN"                     : "Doména:",
+        "FIELD_HEADER_DPI"                        : "Rozlišení (DPI):",
+        "FIELD_HEADER_DRIVE_NAME"                 : "Název jednotky:",
+        "FIELD_HEADER_DRIVE_PATH"                 : "Cesta na disku:",
+        "FIELD_HEADER_ENABLE_AUDIO_INPUT"         : "Povolit zvukový vstup (mikrofon):",
+        "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "Povolit kompozici pracovní plochy (Aero):",
+        "FIELD_HEADER_ENABLE_DRIVE"               : "Povolit jednotku:",
+        "FIELD_HEADER_ENABLE_FONT_SMOOTHING"      : "Povolit vyhlazení písma (ClearType):",
+        "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG"    : "Povolit přetažení celého okna:",
+        "FIELD_HEADER_ENABLE_MENU_ANIMATIONS"     : "Povolit animace nabídky:",
+        "FIELD_HEADER_DISABLE_BITMAP_CACHING"     : "Zakázat ukládání do mezipaměti bitmap:",
+        "FIELD_HEADER_DISABLE_OFFSCREEN_CACHING"  : "Zakázat ukládání do mezipaměti mimo obrazovku:",
+        "FIELD_HEADER_DISABLE_GLYPH_CACHING"      : "Zakázat ukládání do mezipaměti glyfů:",
+        "FIELD_HEADER_ENABLE_PRINTING"            : "Povolit tisk:",
+        "FIELD_HEADER_ENABLE_SFTP"                : "Povolit SFTP:",
+        "FIELD_HEADER_ENABLE_THEMING"             : "Povolit motivy:",
+        "FIELD_HEADER_ENABLE_WALLPAPER"           : "Povolit tapetu:",
+        "FIELD_HEADER_GATEWAY_DOMAIN"             : "Doména:",
+        "FIELD_HEADER_GATEWAY_HOSTNAME"           : "Jméno hostitele:",
+        "FIELD_HEADER_GATEWAY_PASSWORD"           : "Heslo:",
+        "FIELD_HEADER_GATEWAY_PORT"               : "Port:",
+        "FIELD_HEADER_GATEWAY_USERNAME"           : "Uživatelské jméno:",
+        "FIELD_HEADER_HEIGHT"                     : "Výška:",
+        "FIELD_HEADER_HOSTNAME"                   : "Jméno hostitele:",
+        "FIELD_HEADER_IGNORE_CERT"                : "Ignorovat serverový certifikát:",
+        "FIELD_HEADER_INITIAL_PROGRAM"            : "Úvodní program:",
+        "FIELD_HEADER_LOAD_BALANCE_INFO"          : "Vyvážení zátěže info/cookie:",
+        "FIELD_HEADER_PASSWORD"                   : "Heslo:",
+        "FIELD_HEADER_PORT"                       : "Port:",
+        "FIELD_HEADER_PRINTER_NAME"               : "Název přesměrované tiskárny:",
+        "FIELD_HEADER_PRECONNECTION_BLOB"         : "Preconnection BLOB (VM ID):",
+        "FIELD_HEADER_PRECONNECTION_ID"           : "Zdrojové ID RDP",
+        "FIELD_HEADER_READ_ONLY"                  : "Pouze ke čtení:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"    : "Vyloučit myš:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT"   : "Vyloučit grafiku/strímování:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"     : "Zahrnout klíčové události:",
+        "FIELD_HEADER_RECORDING_NAME"             : "Nahrávané jméno:",
+        "FIELD_HEADER_RECORDING_PATH"             : "Nahrávaná cesta:",
+        "FIELD_HEADER_RESIZE_METHOD"              : "Metoda změny velikosti:",
+        "FIELD_HEADER_REMOTE_APP_ARGS"            : "Parametry:",
+        "FIELD_HEADER_REMOTE_APP_DIR"             : "Pracovní adresář:",
+        "FIELD_HEADER_REMOTE_APP"                 : "Program:",
+        "FIELD_HEADER_SECURITY"                   : "Bezpečnostní mód:",
+        "FIELD_HEADER_SERVER_LAYOUT"              : "Rozložení klávesnice:",
+        "FIELD_HEADER_SFTP_DIRECTORY"             : "Výchozí složka pro nahrávání:",
+        "FIELD_HEADER_SFTP_HOST_KEY"              : "Veřejný klíč hosta (Base64)",
+        "FIELD_HEADER_SFTP_HOSTNAME"              : "Jméno hostitele:",
+        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "SFTP keepalive interval:",
+        "FIELD_HEADER_SFTP_PASSPHRASE"            : "Přístupová fráze:",
+        "FIELD_HEADER_SFTP_PASSWORD"              : "Heslo:",
+        "FIELD_HEADER_SFTP_PORT"                  : "Port:",
+        "FIELD_HEADER_SFTP_PRIVATE_KEY"           : "Privátní klíč:",
+        "FIELD_HEADER_SFTP_ROOT_DIRECTORY"        : "Souborový prohlížeč kořenové složky:",
+        "FIELD_HEADER_SFTP_USERNAME"              : "Uživatelské jméno:",
+        "FIELD_HEADER_STATIC_CHANNELS"            : "Názvy statických kanálů:",
+        "FIELD_HEADER_USERNAME"                   : "Uživatelské jméno:",
+        "FIELD_HEADER_WIDTH"                      : "Šířka:",
+
+        "FIELD_OPTION_COLOR_DEPTH_16"    : "Nízké barvy (16-bitů)",
+        "FIELD_OPTION_COLOR_DEPTH_24"    : "Opravdové barvy (24-bitů)",
+        "FIELD_OPTION_COLOR_DEPTH_32"    : "Opravdové barvy (32-bitů)",
+        "FIELD_OPTION_COLOR_DEPTH_8"     : "256 barev",
+        "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "",
+
+        "FIELD_OPTION_RESIZE_METHOD_DISPLAY_UPDATE" : "Virtuální kanál „Aktualizace zobrazení“ (RDP 8.1+)",
+        "FIELD_OPTION_RESIZE_METHOD_EMPTY"     : "",
+        "FIELD_OPTION_RESIZE_METHOD_RECONNECT" : "Znovu připojit",
+
+        "FIELD_OPTION_SECURITY_ANY"   : "Jakýkoliv",
+        "FIELD_OPTION_SECURITY_EMPTY" : "",
+        "FIELD_OPTION_SECURITY_NLA"   : "NLA (Ověřování na úrovni sítě)",
+        "FIELD_OPTION_SECURITY_RDP"   : "Šifrování RDP",
+        "FIELD_OPTION_SECURITY_TLS"   : "Šifrování TLS",
+
+        "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "Němčina (Qwertz)",
+        "FIELD_OPTION_SERVER_LAYOUT_EMPTY"        : "",
+        "FIELD_OPTION_SERVER_LAYOUT_EN_GB_QWERTY" : "UK Angličtina (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US Angličtina (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_ES_ES_QWERTY" : "Španělština (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE"     : "Unicode",
+        "FIELD_OPTION_SERVER_LAYOUT_FR_CH_QWERTZ" : "Švícarská Francouzština (Qwertz)",
+        "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "Francouzština (Azerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italština (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_JA_JP_QWERTY" : "Japonština (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_PT_BR_QWERTY" : "Portugalská Brazilština (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Švédština (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_DA_DK_QWERTY" : "Dánština (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_TR_TR_QWERTY" : "Turečtina (Qwerty)",
+
+        "NAME" : "RDP",
+
+        "SECTION_HEADER_AUTHENTICATION"     : "Ověřování",
+        "SECTION_HEADER_BASIC_PARAMETERS"   : "Základní nastavení",
+        "SECTION_HEADER_CLIPBOARD"          : "Schránka",
+        "SECTION_HEADER_DEVICE_REDIRECTION" : "Přesměrování zařízení",
+        "SECTION_HEADER_DISPLAY"            : "Zobrazení",
+        "SECTION_HEADER_GATEWAY"            : "Brána vzdálené plochy",
+        "SECTION_HEADER_LOAD_BALANCING"     : "Rozložení zátěže",
+        "SECTION_HEADER_NETWORK"            : "Síť",
+        "SECTION_HEADER_PERFORMANCE"        : "Výkon",
+        "SECTION_HEADER_PRECONNECTION_PDU"  : "Preconnection PDU / Hyper-V",
+        "SECTION_HEADER_RECORDING"          : "Nahrávání obrazovky",
+        "SECTION_HEADER_REMOTEAPP"          : "RemoteApp",
+        "SECTION_HEADER_SFTP"               : "SFTP"
+
+    },
+
+    "PROTOCOL_SSH" : {
+
+        "FIELD_HEADER_BACKSPACE"                : "Backspace, poslat klávesy:",
+        "FIELD_HEADER_COLOR_SCHEME"             : "Barva",
+        "FIELD_HEADER_COMMAND"                  : "Provést příkaz:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH"    : "Automaticky vytvořit cestu k nahrávání:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH"   : "Automaticky vytvořit cestu ke strojopisu:",
+        "FIELD_HEADER_DISABLE_COPY"             : "Zakázat kopírování ze vzdáleného terminálu:",
+        "FIELD_HEADER_DISABLE_PASTE"            : "Zakázat vkládání z klienta:",
+        "FIELD_HEADER_FONT_NAME"                : "Typ fontu",
+        "FIELD_HEADER_FONT_SIZE"                : "Velikost písma:",
+        "FIELD_HEADER_ENABLE_SFTP"              : "Povolit SFTP:",
+        "FIELD_HEADER_HOST_KEY"                 : "Veřejný klíč hosta (Base64):",
+        "FIELD_HEADER_HOSTNAME"                 : "Jméno hostitele:",
+        "FIELD_HEADER_USERNAME"                 : "Uživatelské jméno:",
+        "FIELD_HEADER_PASSWORD"                 : "Heslo:",
+        "FIELD_HEADER_PASSPHRASE"               : "Přístupová fráze:",
+        "FIELD_HEADER_PORT"                     : "Port:",
+        "FIELD_HEADER_PRIVATE_KEY"              : "Privátní klíč:",
+        "FIELD_HEADER_READ_ONLY"                : "Pouze ke čtení:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "Vynechat myš:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Vynechat grafiku/streamování:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "Zahrnout události kláves",
+        "FIELD_HEADER_RECORDING_NAME"           : "Nahrávané jméno:",
+        "FIELD_HEADER_RECORDING_PATH"           : "Nahrávaná cesta:",
+        "FIELD_HEADER_SERVER_ALIVE_INTERVAL"    : "Serverový keepalive interval:",
+        "FIELD_HEADER_SFTP_ROOT_DIRECTORY"      : "Souborový prohlížeč kořenové složky:",
+        "FIELD_HEADER_TERMINAL_TYPE"            : "Typ terminálu:",
+        "FIELD_HEADER_TYPESCRIPT_NAME"          : "Jméno strojopisu:",
+        "FIELD_HEADER_TYPESCRIPT_PATH"          : "Cesta ke strojopisu:",
+
+        "FIELD_OPTION_BACKSPACE_EMPTY"          : "",
+        "FIELD_OPTION_BACKSPACE_8"              : "Backspace (Ctrl-H)",
+        "FIELD_OPTION_BACKSPACE_127"            : "Delete (Ctrl-?)",
+        "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Černá na bílé",
+        "FIELD_OPTION_COLOR_SCHEME_EMPTY"       : "",
+        "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK"  : "Šedá na černé",
+        "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Zelená na černé",
+        "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Bílá na černé",
+
+        "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" : "",
+
+        "FIELD_OPTION_TERMINAL_TYPE_ANSI"           : "ansi",
+        "FIELD_OPTION_TERMINAL_TYPE_EMPTY"          : "",
+        "FIELD_OPTION_TERMINAL_TYPE_LINUX"          : "linux",
+        "FIELD_OPTION_TERMINAL_TYPE_VT100"          : "vt100",
+        "FIELD_OPTION_TERMINAL_TYPE_VT220"          : "vt220",
+        "FIELD_OPTION_TERMINAL_TYPE_XTERM"          : "xterm",
+        "FIELD_OPTION_TERMINAL_TYPE_XTERM_256COLOR" : "xterm-256color",
+
+        "NAME" : "SSH",
+
+        "SECTION_HEADER_AUTHENTICATION" : "Ověřování",
+        "SECTION_HEADER_BEHAVIOR"       : "Chování terminálu",
+        "SECTION_HEADER_CLIPBOARD"      : "Schránka",
+        "SECTION_HEADER_DISPLAY"        : "Zobrazení",
+        "SECTION_HEADER_NETWORK"        : "Síť",
+        "SECTION_HEADER_RECORDING"      : "Nahrávání obrazovky",
+        "SECTION_HEADER_SESSION"        : "Sezení / prostředí",
+        "SECTION_HEADER_TYPESCRIPT"     : "Strojopis (textové nahrávání sezení)",
+        "SECTION_HEADER_SFTP"           : "SFTP"
+
+    },
+
+    "PROTOCOL_TELNET" : {
+
+        "FIELD_HEADER_BACKSPACE"                : "Backspace, poslat klávesy:",
+        "FIELD_HEADER_COLOR_SCHEME"             : "Barevné schéma:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH"    : "Automaticky vytvořit cestu k nahrávání:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH"   : "Automaticky vytvořit cestu k typescriptu:",
+        "FIELD_HEADER_DISABLE_COPY"             : "Zakázat kopírování z terminálu:",
+        "FIELD_HEADER_DISABLE_PASTE"            : "Zakázat vkládání z klienta:",
+        "FIELD_HEADER_FONT_NAME"                : "Jméno fontu:",
+        "FIELD_HEADER_FONT_SIZE"                : "Velikost fontu:",
+        "FIELD_HEADER_HOSTNAME"                 : "Jméno hostitele:",
+        "FIELD_HEADER_LOGIN_FAILURE_REGEX"      : "Selhání přihlášení regulární výraz:",
+        "FIELD_HEADER_LOGIN_SUCCESS_REGEX"      : "Úspěch přihlášení regulární výraz:",
+        "FIELD_HEADER_USERNAME"                 : "Uživatelské jméno:",
+        "FIELD_HEADER_USERNAME_REGEX"           : "Uživatelské jméno regulární výraz:",
+        "FIELD_HEADER_PASSWORD"                 : "Heslo:",
+        "FIELD_HEADER_PASSWORD_REGEX"           : "Heslo regulární výraz:",
+        "FIELD_HEADER_PORT"                     : "Port:",
+        "FIELD_HEADER_READ_ONLY"                : "Pouze ke čtení:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "Vyloučit myš:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Vyloučit grafiku/strímování:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "Zahrnout klíčové události:",
+        "FIELD_HEADER_RECORDING_NAME"           : "Nahrávané jméno:",
+        "FIELD_HEADER_RECORDING_PATH"           : "Nahrávaná cesta:",
+        "FIELD_HEADER_TERMINAL_TYPE"            : "Typ terminálu:",
+        "FIELD_HEADER_TYPESCRIPT_NAME"          : "Jméno strojopisu:",
+        "FIELD_HEADER_TYPESCRIPT_PATH"          : "Cesta ke strojopisu:",
+
+        "FIELD_OPTION_BACKSPACE_EMPTY" : "",
+        "FIELD_OPTION_BACKSPACE_8"     : "Backspace (Ctrl-H)",
+        "FIELD_OPTION_BACKSPACE_127"   : "Delete (Ctrl-?)",
+
+        "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Černá na bílé",
+        "FIELD_OPTION_COLOR_SCHEME_EMPTY"       : "",
+        "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK"  : "Šedá na černé",
+        "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Zelená na černé",
+        "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "Bílá na černé",
+
+        "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" : "",
+
+        "FIELD_OPTION_TERMINAL_TYPE_ANSI"           : "ansi",
+        "FIELD_OPTION_TERMINAL_TYPE_EMPTY"          : "",
+        "FIELD_OPTION_TERMINAL_TYPE_LINUX"          : "linux",
+        "FIELD_OPTION_TERMINAL_TYPE_VT100"          : "vt100",
+        "FIELD_OPTION_TERMINAL_TYPE_VT220"          : "vt220",
+        "FIELD_OPTION_TERMINAL_TYPE_XTERM"          : "xterm",
+        "FIELD_OPTION_TERMINAL_TYPE_XTERM_256COLOR" : "xterm-256color",
+
+        "NAME" : "Telnet",
+
+        "SECTION_HEADER_AUTHENTICATION" : "Ověřování",
+        "SECTION_HEADER_BEHAVIOR"       : "Chování terminálu",
+        "SECTION_HEADER_CLIPBOARD"      : "Schránka",
+        "SECTION_HEADER_DISPLAY"        : "Zobrazení",
+        "SECTION_HEADER_RECORDING"      : "Nahrávání obrazovky",
+        "SECTION_HEADER_TYPESCRIPT"     : "Strojopis (textové nahrávání sezení)",
+        "SECTION_HEADER_NETWORK"        : "Síť"
+
+    },
+
+    "PROTOCOL_VNC" : {
+
+        "FIELD_HEADER_AUDIO_SERVERNAME"           : "Název zvukového serveru",
+        "FIELD_HEADER_CLIPBOARD_ENCODING"         : "Kódovávání:",
+        "FIELD_HEADER_COLOR_DEPTH"                : "Hloubka barev:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH"      : "Automaticky vytvořit cestu k nahrávání:",
+        "FIELD_HEADER_CURSOR"                     : "Kurzor:",
+        "FIELD_HEADER_DEST_HOST"                  : "Cílový host:",
+        "FIELD_HEADER_DEST_PORT"                  : "Vzdálený port:",
+        "FIELD_HEADER_DISABLE_COPY"               : "Zakázat kopírování ze vzdálené plochy:",
+        "FIELD_HEADER_DISABLE_PASTE"              : "Zakázat vkládání z klienta:",
+        "FIELD_HEADER_ENABLE_AUDIO"               : "Zapnout audio",
+        "FIELD_HEADER_ENABLE_SFTP"                : "Povolit SFTP:",
+        "FIELD_HEADER_HOSTNAME"                   : "Jméno hostitele:",
+        "FIELD_HEADER_PASSWORD"                   : "Heslo:",
+        "FIELD_HEADER_PORT"                       : "Port:",
+        "FIELD_HEADER_READ_ONLY"                  : "Pouze čtení:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"    : "Vynechat myš:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT"   : "Vynechat grafiku/stremování:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"     : "Zahrnout události kláves",
+        "FIELD_HEADER_RECORDING_NAME"             : "Nahrávané jméno:",
+        "FIELD_HEADER_RECORDING_PATH"             : "Nahrávaná cesta:",
+        "FIELD_HEADER_SFTP_DIRECTORY"             : "Výchozí složka pro nahrávání:",
+        "FIELD_HEADER_SFTP_HOST_KEY"              : "Veřejný klíč hosta (Base64)",
+        "FIELD_HEADER_SFTP_HOSTNAME"              : "Jméno hostitele:",
+        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "SFTP keepalive interval:",
+        "FIELD_HEADER_SFTP_PASSPHRASE"            : "Přístupová fráze:",
+        "FIELD_HEADER_SFTP_PASSWORD"              : "Heslo:",
+        "FIELD_HEADER_SFTP_PORT"                  : "Port:",
+        "FIELD_HEADER_SFTP_PRIVATE_KEY"           : "Privátní klíč:",
+        "FIELD_HEADER_SFTP_ROOT_DIRECTORY"        : "Souborový prohlížeč kořenové složky:",
+        "FIELD_HEADER_SFTP_USERNAME"              : "Uživatelské jméno:",
+        "FIELD_HEADER_SWAP_RED_BLUE"              : "Přehodit červené/modré komponenty:",
+
+        "FIELD_OPTION_COLOR_DEPTH_8"     : "256 barev",
+        "FIELD_OPTION_COLOR_DEPTH_16"    : "Nízké barvy (16-bitů)",
+        "FIELD_OPTION_COLOR_DEPTH_24"    : "Opravdové barvy (24-bitů)",
+        "FIELD_OPTION_COLOR_DEPTH_32"    : "Opravdové barvy (32-bitů)",
+        "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "",
+
+        "FIELD_OPTION_CURSOR_EMPTY"  : "",
+        "FIELD_OPTION_CURSOR_LOCAL"  : "Místní",
+        "FIELD_OPTION_CURSOR_REMOTE" : "Vzdálený",
+
+        "FIELD_OPTION_CLIPBOARD_ENCODING_CP1252"    : "CP1252",
+        "FIELD_OPTION_CLIPBOARD_ENCODING_EMPTY"     : "",
+        "FIELD_OPTION_CLIPBOARD_ENCODING_ISO8859_1" : "ISO 8859-1",
+        "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_8"     : "UTF-8",
+        "FIELD_OPTION_CLIPBOARD_ENCODING_UTF_16"    : "UTF-16",
+
+        "NAME" : "VNC",
+
+        "SECTION_HEADER_AUDIO"          : "Zvuk",
+        "SECTION_HEADER_AUTHENTICATION" : "Ověřování",
+        "SECTION_HEADER_CLIPBOARD"      : "Schránka",
+        "SECTION_HEADER_DISPLAY"        : "Zobrazení",
+        "SECTION_HEADER_NETWORK"        : "Síť",
+        "SECTION_HEADER_RECORDING"      : "Nahrávání obrazovky",
+        "SECTION_HEADER_REPEATER"       : "VNC opakovač",
+        "SECTION_HEADER_SFTP"           : "SFTP"
+
+    },
+
+    "SETTINGS" : {
+
+        "SECTION_HEADER_SETTINGS" : "Nastavení"
+
+    },
+
+    "SETTINGS_CONNECTION_HISTORY" : {
+
+        "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD",
+        "ACTION_SEARCH"   : "@:APP.ACTION_SEARCH",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FILENAME_HISTORY_CSV" : "history.csv",
+
+        "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_CONNECTION_HISTORY" : "Zde jsou uvedeny záznamy o historii připojení a lze je třídit kliknutím na záhlaví sloupců. Chcete-li vyhledat konkrétní záznamy, zadejte řetězec filtrů a klikněte na tlačítko Hledat. Zobrazí se pouze záznamy, které odpovídají zadanému řetězci filtrů.",
+
+        "INFO_CONNECTION_DURATION_UNKNOWN" : "--",
+        "INFO_NO_HISTORY"                  : "Žádné shodné záznamy",
+
+        "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Název připojení",
+        "TABLE_HEADER_SESSION_DURATION"        : "Doba trvání",
+        "TABLE_HEADER_SESSION_REMOTEHOST"      : "Vzdálený host",
+        "TABLE_HEADER_SESSION_STARTDATE"       : "Počáteční čas",
+        "TABLE_HEADER_SESSION_USERNAME"        : "Uživatelské jméno",
+
+        "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION"
+
+    },
+
+    "SETTINGS_CONNECTIONS" : {
+
+        "ACTION_ACKNOWLEDGE"          : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_NEW_CONNECTION"       : "Nové připojení",
+        "ACTION_NEW_CONNECTION_GROUP" : "Nový skupina",
+        "ACTION_NEW_SHARING_PROFILE"  : "Nový sdílený profil",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_CONNECTIONS" : "Toto připojení můžete spravovat klepnutím nebo klepnutím na níže uvedené připojení. V závislosti na vaší úrovni přístupu lze přidávat a mazat připojení a měnit jejich vlastnosti (protokol, název hostitele, port atd.).",
+
+        "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT",
+
+        "SECTION_HEADER_CONNECTIONS" : "Připojení"
+
+    },
+
+    "SETTINGS_PREFERENCES" : {
+
+        "ACTION_ACKNOWLEDGE"     : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"          : "@:APP.ACTION_CANCEL",
+        "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "ERROR_PASSWORD_BLANK"    : "@:APP.ERROR_PASSWORD_BLANK",
+        "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
+
+        "FIELD_HEADER_LANGUAGE"           : "Jazyk zobrazení:",
+        "FIELD_HEADER_PASSWORD"           : "Heslo:",
+        "FIELD_HEADER_PASSWORD_OLD"       : "Aktuální heslo:",
+        "FIELD_HEADER_PASSWORD_NEW"       : "Nové heslo:",
+        "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Potvrďte nové heslo:",
+        "FIELD_HEADER_USERNAME"           : "Uživatelské jméno:",
+
+        "HELP_DEFAULT_INPUT_METHOD" : "Výchozí metoda vstupu určuje, jak Guacamole přijímá události klávesnice. Změna tohoto nastavení může být nezbytná při používání mobilního zařízení nebo při psaní přes IME. Toto nastavení může být přepsáno na základě připojení v rámci nabídky Guacamole.",
+        "HELP_DEFAULT_MOUSE_MODE"   : "Výchozí režim emulace myši určuje, jak se bude vzdálená myš chovat v nových spojeních s ohledem na dotyky. Toto nastavení může být přepsáno na základě připojení v rámci nabídky Guacamole.",
+        "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"             : "Chcete-li změnit jazyk celého textu v Guacamole, vyberte níže uvedený jazyk. Dostupné volby budou záviset na nainstalovaných jazycích.",
+        "HELP_MOUSE_MODE_ABSOLUTE"  : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE",
+        "HELP_MOUSE_MODE_RELATIVE"  : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE",
+        "HELP_UPDATE_PASSWORD"      : "Pokud chcete změnit heslo, zadejte své aktuální heslo a níže požadované nové heslo a klikněte na tlačítko „Aktualizovat heslo“. Změna se projeví okamžitě.",
+
+        "INFO_PASSWORD_CHANGED" : "Heslo bylo změněno.",
+
+        "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE",
+        "NAME_INPUT_METHOD_OSK"  : "@:CLIENT.NAME_INPUT_METHOD_OSK",
+        "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT",
+
+        "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Výchozí metoda vstupu",
+        "SECTION_HEADER_DEFAULT_MOUSE_MODE"   : "Výchozí mód emulace myši",
+        "SECTION_HEADER_UPDATE_PASSWORD"      : "Změnit heslo"
+
+    },
+
+    "SETTINGS_USERS" : {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_NEW_USER"    : "Nový uživatel",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_USERS" : "Chcete-li spravovat daného uživatele, klepněte na něj nebo klepněte na něj. V závislosti na úrovni přístupu mohou být uživatelé přidáváni a mazáni a jejich hesla mohou být změněna.",
+
+        "SECTION_HEADER_USERS" : "Uživatel",
+
+        "TABLE_HEADER_FULL_NAME"    : "Celé jméno",
+        "TABLE_HEADER_LAST_ACTIVE"  : "Poslední aktivní",
+        "TABLE_HEADER_ORGANIZATION" : "Organizace",
+        "TABLE_HEADER_USERNAME"     : "Uživatelské jméno"
+
+    },
+
+    "SETTINGS_USER_GROUPS" : {
+
+        "ACTION_ACKNOWLEDGE"    : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_NEW_USER_GROUP" : "Nová skupina",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_USER_GROUPS" : "Chcete-li tuto skupinu spravovat, klepněte na ni nebo klepněte na ni. V závislosti na úrovni přístupu lze skupiny přidávat a mazat a jejich členské uživatele a skupiny lze měnit.",
+
+        "SECTION_HEADER_USER_GROUPS" : "Skupiny",
+
+        "TABLE_HEADER_USER_GROUP_NAME" : "Jméno skupiny"
+
+    },
+
+    "SETTINGS_SESSIONS" : {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"      : "@:APP.ACTION_CANCEL",
+        "ACTION_DELETE"      : "Ukončit sezení",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "Ukončit sezení",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_SESSIONS" : "Tato stránka bude naplněna aktuálně aktivními připojeními. Uvedená připojení a schopnost zabít tato připojení závisí na úrovni přístupu. Pokud chcete zabít jednu nebo více relací, zaškrtněte políčko vedle těchto relací a klepněte na tlačítko \"Zabít relace\". Zabití relace okamžitě odpojí uživatele od přidruženého připojení.",
+
+        "INFO_NO_SESSIONS" : "Žádné aktivní sezení",
+
+        "SECTION_HEADER_SESSIONS" : "Aktivní sezení",
+
+        "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Název připojení",
+        "TABLE_HEADER_SESSION_REMOTEHOST"      : "Vzdálený host",
+        "TABLE_HEADER_SESSION_STARTDATE"       : "Aktivní od",
+        "TABLE_HEADER_SESSION_USERNAME"        : "Uživatelské jméno:",
+
+        "TEXT_CONFIRM_DELETE" : "Jste si jisti, že chcete ukončit vybrané sezení? Uživatele užívající toto spojení budou okamžitě odpojeni. "
+
+    },
+
+    "USER_ATTRIBUTES" : {
+
+        "FIELD_HEADER_GUAC_EMAIL_ADDRESS"       : "Emailová adresa:",
+        "FIELD_HEADER_GUAC_FULL_NAME"           : "Celé jméno:",
+        "FIELD_HEADER_GUAC_ORGANIZATION"        : "Organizace:",
+        "FIELD_HEADER_GUAC_ORGANIZATIONAL_ROLE" : "Role:"
+
+    },
+
+    "USER_MENU" : {
+
+        "ACTION_LOGOUT"             : "@:APP.ACTION_LOGOUT",
+        "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS",
+        "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES",
+        "ACTION_MANAGE_SESSIONS"    : "@:APP.ACTION_MANAGE_SESSIONS",
+        "ACTION_MANAGE_SETTINGS"    : "@:APP.ACTION_MANAGE_SETTINGS",
+        "ACTION_MANAGE_USERS"       : "@:APP.ACTION_MANAGE_USERS",
+        "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS",
+        "ACTION_NAVIGATE_HOME"      : "@:APP.ACTION_NAVIGATE_HOME",
+        "ACTION_VIEW_HISTORY"       : "@:APP.ACTION_VIEW_HISTORY"
+
+    }
+
+}
diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json
index 5d240a0..4e6420c 100644
--- a/guacamole/src/main/webapp/translations/en.json
+++ b/guacamole/src/main/webapp/translations/en.json
@@ -150,6 +150,22 @@
 
     },
 
+    "COLOR_SCHEME" : {
+
+        "ACTION_CANCEL"       : "@:APP.ACTION_CANCEL",
+        "ACTION_HIDE_DETAILS" : "Hide",
+        "ACTION_SAVE"         : "@:APP.ACTION_SAVE",
+        "ACTION_SHOW_DETAILS" : "Show",
+
+        "FIELD_HEADER_BACKGROUND" : "Background",
+        "FIELD_HEADER_FOREGROUND" : "Foreground",
+
+        "FIELD_OPTION_CUSTOM" : "Custom...",
+
+        "SECTION_HEADER_DETAILS" : "Details:"
+
+    },
+
     "DATA_SOURCE_DEFAULT" : {
         "NAME" : "Default (XML)"
     },
@@ -364,7 +380,73 @@
         "TEXT_CONFIRM_DELETE" : "Groups cannot be restored after they have been deleted. Are you sure you want to delete this group?"
 
     },
-    
+
+    "PROTOCOL_KUBERNETES" : {
+
+        "FIELD_HEADER_BACKSPACE"       : "Backspace key sends:",
+        "FIELD_HEADER_CA_CERT"         : "Certificate authority certificate:",
+        "FIELD_HEADER_CLIENT_CERT"     : "Client certificate:",
+        "FIELD_HEADER_CLIENT_KEY"      : "Client key:",
+        "FIELD_HEADER_COLOR_SCHEME"    : "Color scheme:",
+        "FIELD_HEADER_CONTAINER"       : "Container name:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH"  : "Automatically create recording path:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automatically create typescript path:",
+        "FIELD_HEADER_FONT_NAME"       : "Font name:",
+        "FIELD_HEADER_FONT_SIZE"       : "Font size:",
+        "FIELD_HEADER_HOSTNAME"        : "Hostname:",
+        "FIELD_HEADER_IGNORE_CERT"     : "Ignore server certificate:",
+        "FIELD_HEADER_NAMESPACE"       : "Namespace:",
+        "FIELD_HEADER_POD"             : "Pod name:",
+        "FIELD_HEADER_PORT"            : "Port:",
+        "FIELD_HEADER_READ_ONLY"       : "Read-only:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "Exclude mouse:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclude graphics/streams:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "Include key events:",
+        "FIELD_HEADER_RECORDING_NAME"  : "Recording name:",
+        "FIELD_HEADER_RECORDING_PATH"  : "Recording path:",
+        "FIELD_HEADER_SCROLLBACK"      : "Maximum scrollback size:",
+        "FIELD_HEADER_TYPESCRIPT_NAME" : "Typescript name:",
+        "FIELD_HEADER_TYPESCRIPT_PATH" : "Typescript path:",
+        "FIELD_HEADER_USE_SSL"         : "Use SSL/TLS",
+
+        "FIELD_OPTION_BACKSPACE_EMPTY" : "",
+        "FIELD_OPTION_BACKSPACE_8"     : "Backspace (Ctrl-H)",
+        "FIELD_OPTION_BACKSPACE_127"   : "Delete (Ctrl-?)",
+
+        "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "Black on white",
+        "FIELD_OPTION_COLOR_SCHEME_EMPTY"       : "",
+        "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK"  : "Gray on black",
+        "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "Green on black",
+        "FIELD_OPTION_COLOR_SCHEME_WHITE_BLACK" : "White on black",
+
+        "FIELD_OPTION_FONT_SIZE_8"     : "8",
+        "FIELD_OPTION_FONT_SIZE_9"     : "9",
+        "FIELD_OPTION_FONT_SIZE_10"    : "10",
+        "FIELD_OPTION_FONT_SIZE_11"    : "11",
+        "FIELD_OPTION_FONT_SIZE_12"    : "12",
+        "FIELD_OPTION_FONT_SIZE_14"    : "14",
+        "FIELD_OPTION_FONT_SIZE_18"    : "18",
+        "FIELD_OPTION_FONT_SIZE_24"    : "24",
+        "FIELD_OPTION_FONT_SIZE_30"    : "30",
+        "FIELD_OPTION_FONT_SIZE_36"    : "36",
+        "FIELD_OPTION_FONT_SIZE_48"    : "48",
+        "FIELD_OPTION_FONT_SIZE_60"    : "60",
+        "FIELD_OPTION_FONT_SIZE_72"    : "72",
+        "FIELD_OPTION_FONT_SIZE_96"    : "96",
+        "FIELD_OPTION_FONT_SIZE_EMPTY" : "",
+
+        "NAME" : "Kubernetes",
+
+        "SECTION_HEADER_AUTHENTICATION" : "Authentication",
+        "SECTION_HEADER_BEHAVIOR"       : "Terminal behavior",
+        "SECTION_HEADER_CONTAINER"      : "Container",
+        "SECTION_HEADER_DISPLAY"        : "Display",
+        "SECTION_HEADER_RECORDING"      : "Screen Recording",
+        "SECTION_HEADER_TYPESCRIPT"     : "Typescript (Text Session Recording)",
+        "SECTION_HEADER_NETWORK"        : "Network"
+
+    },
+
     "PROTOCOL_RDP" : {
 
         "FIELD_HEADER_CLIENT_NAME"     : "Client name:",
@@ -432,6 +514,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:",
 
@@ -451,6 +534,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)",
@@ -459,6 +543,7 @@
         "FIELD_OPTION_SERVER_LAYOUT_FAILSAFE"     : "Unicode",
         "FIELD_OPTION_SERVER_LAYOUT_FR_CH_QWERTZ" : "Swiss French (Qwertz)",
         "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "French (Azerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_HU_HU_QWERTZ" : "Hungarian (Qwertz)",        
         "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italian (Qwerty)",
         "FIELD_OPTION_SERVER_LAYOUT_JA_JP_QWERTY" : "Japanese (Qwerty)",
         "FIELD_OPTION_SERVER_LAYOUT_PT_BR_QWERTY" : "Portuguese Brazilian (Qwerty)",
@@ -498,11 +583,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:",
@@ -512,6 +599,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:",
 
@@ -587,6 +675,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:",
@@ -767,6 +856,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.",
@@ -774,7 +864,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/ja.json b/guacamole/src/main/webapp/translations/ja.json
new file mode 100644
index 0000000..e3bad5e
--- /dev/null
+++ b/guacamole/src/main/webapp/translations/ja.json
@@ -0,0 +1,746 @@
+{
+    
+    "NAME" : "日本語",
+    
+    "APP" : {
+
+        "ACTION_CANCEL"             : "キャンセル",
+        "ACTION_CLONE"              : "コピー",
+        "ACTION_CONTINUE"           : "次へ",
+        "ACTION_DELETE"             : "削除",
+        "ACTION_DELETE_SESSIONS"    : "セッションの切断",
+        "ACTION_DOWNLOAD"           : "ダウンロード",
+        "ACTION_LOGIN"              : "ログイン",
+        "ACTION_LOGOUT"             : "ログアウト",
+        "ACTION_MANAGE_CONNECTIONS" : "接続",
+        "ACTION_MANAGE_PREFERENCES" : "ユーザ設定",
+        "ACTION_MANAGE_SETTINGS"    : "設定",
+        "ACTION_MANAGE_SESSIONS"    : "アクティブなセッション",
+        "ACTION_MANAGE_USERS"       : "ユーザ",
+        "ACTION_MANAGE_USER_GROUPS" : "グループ",
+        "ACTION_NAVIGATE_BACK"      : "戻る",
+        "ACTION_NAVIGATE_HOME"      : "ホーム",
+        "ACTION_SAVE"               : "保存",
+        "ACTION_SEARCH"             : "検索",
+        "ACTION_SHARE"              : "シェア",
+        "ACTION_UPDATE_PASSWORD"    : "パスワードの更新",
+        "ACTION_VIEW_HISTORY"       : "履歴",
+
+        "DIALOG_HEADER_ERROR" : "エラー",
+
+        "ERROR_PAGE_UNAVAILABLE"  : "エラーが発生したため、この操作を完了できませんでした。問題が解決しない場合は、システム管理者に連絡するか、システムログを確認してください。",
+
+        "ERROR_PASSWORD_BLANK"    : "パスワードが入力されていません。",
+        "ERROR_PASSWORD_MISMATCH" : "パスワードが一致しません。",
+        
+        "FIELD_HEADER_PASSWORD"       : "パスワード:",
+        "FIELD_HEADER_PASSWORD_AGAIN" : "パスワード確認:",
+
+        "FIELD_PLACEHOLDER_FILTER" : "フィルタ",
+
+        "INFO_ACTIVE_USER_COUNT" : "現在以下のユーザが利用中です。 {USERS} {USERS, plural, one{user} other{users}}.",
+
+        "TEXT_ANONYMOUS_USER"   : "匿名ユーザ"
+
+    },
+
+    "CLIENT" : {
+
+        "ACTION_ACKNOWLEDGE"               : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CLEAR_COMPLETED_TRANSFERS" : "クリア",
+        "ACTION_DISCONNECT"                : "切断",
+        "ACTION_LOGOUT"                    : "@:APP.ACTION_LOGOUT",
+        "ACTION_NAVIGATE_BACK"             : "@:APP.ACTION_NAVIGATE_BACK",
+        "ACTION_NAVIGATE_HOME"             : "@:APP.ACTION_NAVIGATE_HOME",
+        "ACTION_RECONNECT"                 : "再接続",
+        "ACTION_SAVE_FILE"                 : "@:APP.ACTION_SAVE",
+        "ACTION_SHARE"                     : "@:APP.ACTION_SHARE",
+        "ACTION_UPLOAD_FILES"              : "ファイルアップロード",
+
+        "DIALOG_HEADER_CONNECTING"       : "接続",
+        "DIALOG_HEADER_CONNECTION_ERROR" : "接続エラー",
+        "DIALOG_HEADER_DISCONNECTED"     : "切断",
+
+        "ERROR_CLIENT_201": "サーバーがビジー状態のため、この接続は切断されました。数分待ってからもう一度お試しください。",
+        "ERROR_CLIENT_202": "リモートデスクトップの応答に時間がかかりすぎるため、Guacamoleサーバが接続を切断しました。再試行するか、システム管理者に連絡してください。",
+        "ERROR_CLIENT_203": "リモートデスクトップサーバーでエラーが発生し、接続を切断しました。再試行するかシステム管理者に連絡してください。",
+        "ERROR_CLIENT_207": "現在リモートデスクトップサーバーにアクセスできません。問題が解決しない場合、システム管理者に連絡するか、システムログを確認してください。",
+        "ERROR_CLIENT_208": "リモートデスクトップサーバーは現在利用できません。問題が解決しない場合、システム管理者に連絡するか、システムログを確認してください。",
+        "ERROR_CLIENT_209": "リモートデスクトップサーバーが他の接続と競合しています。後でもう一度やり直してください。",
+        "ERROR_CLIENT_20A": "リモートデスクトップサーバーが停止しているため接続を閉じました。システム管理者に連絡するか、システム設定を確認してください。",
+        "ERROR_CLIENT_20B": "リモートデスクトップサーバーが強制的に接続を切断しました。システム管理者に連絡するか、システムログを確認してください。",
+        "ERROR_CLIENT_301": "ログインに失敗しました。再接続してからもう一度お試しください。",
+        "ERROR_CLIENT_303": "リモートデスクトップサーバーがこの接続へのアクセスを拒否しました。アクセスが必要な場合は、システム管理者にアカウントのアクセスを許可を依頼するか、システム設定を確認してください。",
+        "ERROR_CLIENT_308": "ブラウザからの応答が十分でないため、Guacamoleサーバーが接続を切断しました。これは一般的にネットワークの問題が原因です。ネットワークの状態を確認して、もう一度やり直してください。 ",
+        "ERROR_CLIENT_31D": "同時接続の使用制限に達したため、Guacamoleサーバーはこの接続へのアクセスを拒否しています。1つ以上の接続を閉じてからやり直してください。",
+        "ERROR_CLIENT_DEFAULT": "Guacamoleサーバ内で内部エラーが発生し、接続が終了しました。問題が解決しない場合、システム管理者に連絡するか、システムログを確認してください。",
+
+        "ERROR_TUNNEL_201": "アクティブな接続が多すぎるため、Guacamoleサーバーはこの接続を拒否しました。数分待ってからもう一度お試しください。",
+        "ERROR_TUNNEL_202": "サーバーの応答に時間がかかりすぎるため、接続が切断されました。通常はネットワーク問題によって引き起こされます。",
+        "ERROR_TUNNEL_203": "サーバーでエラーが発生し、接続を切断しました。再試行するかシステム管理者に連絡してください。",
+        "ERROR_TUNNEL_204": "指定された接続は存在しません。接続名を確認してもう一度やり直してください。",
+        "ERROR_TUNNEL_205": "この接続は現在使用中です。同時アクセスは許可されていません。後ほどやり直してください。",
+        "ERROR_TUNNEL_207": "現在Guacamoleサーバーにアクセスできません。ネットワークの状態を確認してもう一度やり直してください。",
+        "ERROR_TUNNEL_208": "Guacamoleサーバーは接続を受け付けていません。ネットワークの状態を確認してもう一度やり直してください。",
+        "ERROR_TUNNEL_301": "あなたはログインしていないため、この接続にアクセスする権限がありません。ログインしてからやり直してください。",
+        "ERROR_TUNNEL_303": "この接続にアクセスする権限がありません。アクセスが必要な場合は、システム管理者に許可を依頼するか、システム設定を確認してください。",
+        "ERROR_TUNNEL_308": "ブラウザからの応答がないため、Guacamoleサーバーが接続を切断しました。一般的にネットワークの問題が原因です。ネットワークの状態を確認して、もう一度やり直してください。 ",
+        "ERROR_TUNNEL_31D": "同時接続の使用制限に達したため、Guacamoleサーバーはこの接続へのアクセスを拒否しています。1つ以上の接続を閉じてからやり直してください。",
+        "ERROR_TUNNEL_DEFAULT": "Guacamoleサーバー内で内部エラーが発生し、接続が終了しました。問題が解決しない場合、システム管理者に連絡するか、システムログを確認してください。",
+
+        "ERROR_UPLOAD_100": "ファイル転送がサポートされていないか有効になっていません。システム管理者に連絡するか、システムログを確認してください。",
+        "ERROR_UPLOAD_201": "現在転送中のファイルが多すぎます。転送が完了するのを待ってからやり直してください。",
+        "ERROR_UPLOAD_202": "リモートデスクトップサーバーの応答に時間がかかりすぎるため、ファイルを転送できません。やりなおすかシステム管理者に連絡してください。",
+        "ERROR_UPLOAD_203": "転送中にリモートデスクトップサーバーでエラーが発生しました。もう一度やり直すか、システム管理者に連絡してください。",
+        "ERROR_UPLOAD_204": "ファイル転送先が存在しません。宛先が存在することを確認してやり直してください。",
+        "ERROR_UPLOAD_205": "ファイル転送先は現在ロックされています。進行中のタスクが完了するのを待ってからやり直してください。 ",
+        "ERROR_UPLOAD_301": "あなたはログインしていないため、このファイルをアップロードする権限がありません。ログインしてからやり直してください。",
+        "ERROR_UPLOAD_303": "このファイルをアップロードする権限がありません。アクセスが必要な場合は、システム設定を確認するか、システム管理者に確認してください。",
+        "ERROR_UPLOAD_308": "ファイル転送が停止しています。これは一般的にネットワーク速度が非常に遅いなどのネットワークの問題が原因で発生します。",
+        "ERROR_UPLOAD_31D": "現在転送中のファイルが多すぎます。転送が完了するのを待ってからやり直してください。",
+        "ERROR_UPLOAD_DEFAULT": "Guacamoleサーバ内で内部エラーが発生し、接続が終了しました。それでも問題が解決しない場合、システム管理者に連絡するか、システムログを確認してください。",
+
+        "HELP_CLIPBOARD": "Guacamole内でコピー/カットされたテキストがここに表示されます。以下のテキストを変更するとリモートクリップボードに影響します。 ",
+        "HELP_INPUT_METHOD_NONE": "入力方法は指定されていません。キーボード入力は接続された物理的なキーボードから受け付けられます。 ",
+        "HELP_INPUT_METHOD_OSK": "内蔵のGuacamoleオンスクリーンキーボードからの入力を表示し、受け入れます。オンスクリーンキーボードを使用すると、不可能な場合もあるキーの組み合わせを入力できます(Ctrl-Alt-Delなど)。",
+        "HELP_INPUT_METHOD_TEXT": "テキストの入力を許可し、入力されたテキストに基づいてキーボードイベントをエミュレートします。これはスマートフォンのような物理的なキーボードがない機器に必要です。 ",
+        "HELP_MOUSE_MODE": "タッチに対するリモートマウスの動作を決定します。 ",
+        "HELP_MOUSE_MODE_ABSOLUTE": "タッチによってクリックを行います。タッチの位置でクリックしたとみなされます。 ",
+        "HELP_MOUSE_MODE_RELATIVE": "マウスポインタをドラッグしてからクリックします。マウスポインタの位置でクリックしたとみなされます。 ",
+        "HELP_SHARE_LINK": "現在の接続は共有されており、次の{LINKS、multiple、one {link} other {links}}を持つ人なら誰でもアクセスできます:",
+
+        "INFO_CONNECTION_SHARED": "この接続は現在共有されています。",
+        "INFO_NO_FILE_TRANSFERS": "ファイル転送はありません。",
+
+
+        "NAME_INPUT_METHOD_NONE"   : "なし",
+        "NAME_INPUT_METHOD_OSK"    : "オンスクリーンキーボード",
+        "NAME_INPUT_METHOD_TEXT"   : "テキストインプット",
+        "NAME_MOUSE_MODE_ABSOLUTE" : "タッチスクリーン",
+        "NAME_MOUSE_MODE_RELATIVE" : "タッチパッド",
+
+        "SECTION_HEADER_CLIPBOARD"      : "クリップボード",
+        "SECTION_HEADER_DEVICES"        : "デバイス",
+        "SECTION_HEADER_DISPLAY"        : "ディスプレイ",
+        "SECTION_HEADER_FILE_TRANSFERS" : "ファイル転送",
+        "SECTION_HEADER_INPUT_METHOD"   : "インプットメソッド",
+        "SECTION_HEADER_MOUSE_MODE"     : "マウスエミュレートモード",
+
+        "TEXT_ZOOM_AUTO_FIT"              : "自動的にブラウザのサイズに合わせる",
+        "TEXT_CLIENT_STATUS_IDLE"         : "アイドル状態.",
+        "TEXT_CLIENT_STATUS_CONNECTING"   : "Guacamoleサーバに接続しています...",
+        "TEXT_CLIENT_STATUS_DISCONNECTED" : "切断されました。",
+        "TEXT_CLIENT_STATUS_UNSTABLE"     : "Guacamoleサーバへのネットワーク接続が不安定です。",
+        "TEXT_CLIENT_STATUS_WAITING"      : "Guacamoleサーバに接続しました。応答を待っています",
+        "TEXT_RECONNECT_COUNTDOWN"        : "再接続しています... {REMAINING} {REMAINING, plural, one{second} other{seconds}}..."
+
+    },
+
+    "FORM" : {
+
+        "HELP_SHOW_PASSWORD" : "パスワードを見る",
+        "HELP_HIDE_PASSWORD" : "パスワードを隠す"
+
+    },
+
+    "HOME" : {
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT",
+
+        "INFO_NO_RECENT_CONNECTIONS" : "最近の接続情報はありません。",
+        
+        "PASSWORD_CHANGED" : "パスワードが変更されました。",
+
+        "SECTION_HEADER_ALL_CONNECTIONS"    : "全ての接続情報",
+        "SECTION_HEADER_RECENT_CONNECTIONS" : "最近の接続情報"
+
+    },
+
+    "LIST" : {
+
+        "TEXT_ANONYMOUS_USER" : "匿名"
+
+    },
+
+    "LOGIN": {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CONTINUE"    : "@:APP.ACTION_CONTINUE",
+        "ACTION_LOGIN"       : "@:APP.ACTION_LOGIN",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "ERROR_INVALID_LOGIN" : "不正なログインです。",
+
+        "FIELD_HEADER_USERNAME" : "ユーザ名",
+        "FIELD_HEADER_PASSWORD" : "パスワード"
+
+    },
+
+    "MANAGE_CONNECTION" : {
+
+        "ACTION_ACKNOWLEDGE"          : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"               : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"                : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"               : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"                 : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "接続の削除",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_LOCATION" : "ロケーション:",
+        "FIELD_HEADER_NAME"     : "名前:",
+        "FIELD_HEADER_PROTOCOL" : "プロトコル:",
+
+        "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "INFO_CONNECTION_DURATION_UNKNOWN" : "--",
+        "INFO_CONNECTION_ACTIVE_NOW"       : "アクティブにする",
+        "INFO_CONNECTION_NOT_USED"         : "この接続はまだ使用されていません。",
+
+        "SECTION_HEADER_EDIT_CONNECTION" : "接続の編集",
+        "SECTION_HEADER_HISTORY"         : "使用履歴",
+        "SECTION_HEADER_PARAMETERS"      : "パラメータ",
+
+        "TABLE_HEADER_HISTORY_USERNAME"   : "ユーザ名",
+        "TABLE_HEADER_HISTORY_START"      : "開始時間",
+        "TABLE_HEADER_HISTORY_DURATION"   : "期間",
+        "TABLE_HEADER_HISTORY_REMOTEHOST" : "接続元",
+
+        "TEXT_CONFIRM_DELETE"   : "削除した接続は元に戻せません。この接続を削除してもよろしいですか?",
+        "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION"
+
+    },
+
+    "MANAGE_CONNECTION_GROUP" : {
+
+        "ACTION_ACKNOWLEDGE"   : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"        : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"         : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"        : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"          : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "接続グループの削除",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_LOCATION" : "ロケーション:",
+        "FIELD_HEADER_NAME"     : "名前:",
+        "FIELD_HEADER_TYPE"     : "タイプ:",
+
+        "NAME_TYPE_BALANCING"       : "バランシング",
+        "NAME_TYPE_ORGANIZATIONAL"  : "組織",
+
+        "SECTION_HEADER_EDIT_CONNECTION_GROUP" : "接続グループの編集",
+
+        "TEXT_CONFIRM_DELETE" : "接続グループを削除した後に復元することはできません。この接続グループを削除してもよろしいですか?"
+
+    },
+
+    "MANAGE_SHARING_PROFILE" : {
+
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"      : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"       : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"      : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"        : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "共有プロファイルの削除",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_NAME"               : "名前:",
+        "FIELD_HEADER_PRIMARY_CONNECTION" : "プライマリ接続:",
+
+        "SECTION_HEADER_EDIT_SHARING_PROFILE" : "共有プロファイルの編集",
+        "SECTION_HEADER_PARAMETERS"           : "パラメータ",
+
+        "TEXT_CONFIRM_DELETE" : "削除した共有プロファイルは復元できません。この共有プロファイルを削除してもよろしいですか?"
+
+    },
+
+    "MANAGE_USER" : {
+
+        "ACTION_ACKNOWLEDGE"   : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"        : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"         : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"        : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"          : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "ユーザ削除",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
+
+        "FIELD_HEADER_ADMINISTER_SYSTEM"             : "システム管理者:",
+        "FIELD_HEADER_CHANGE_OWN_PASSWORD"           : "自身のパスワードの変更:",
+        "FIELD_HEADER_CREATE_NEW_USERS"              : "ユーザの作成:",
+        "FIELD_HEADER_CREATE_NEW_USER_GROUPS"        : "ユーザグループの作成:",
+        "FIELD_HEADER_CREATE_NEW_CONNECTIONS"        : "接続の作成:",
+        "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS"  : "接続グループの作成:",
+        "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES"   : "共有プロファイルの作成:",
+        "FIELD_HEADER_PASSWORD"                      : "@:APP.FIELD_HEADER_PASSWORD",
+        "FIELD_HEADER_PASSWORD_AGAIN"                : "@:APP.FIELD_HEADER_PASSWORD_AGAIN",
+        "FIELD_HEADER_USERNAME"                      : "ユーザ名:",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_NO_USER_GROUPS" : "このユーザーは現在どのグループにも属していません。このセクションを展開してグループを追加してください。",
+
+        "INFO_READ_ONLY"                : "このユーザは編集できません。",
+        "INFO_NO_USER_GROUPS_AVAILABLE" : "利用可能なグループがありません。",
+
+        "SECTION_HEADER_ALL_CONNECTIONS"     : "すべての接続",
+        "SECTION_HEADER_CONNECTIONS"         : "接続",
+        "SECTION_HEADER_CURRENT_CONNECTIONS" : "現在の接続",
+        "SECTION_HEADER_EDIT_USER"           : "ユーザの編集",
+        "SECTION_HEADER_PERMISSIONS"         : "権限",
+        "SECTION_HEADER_USER_GROUPS"         : "グループ",
+
+        "TEXT_CONFIRM_DELETE" : "削除したユーザーは元に戻せません。このユーザーを削除してもよろしいですか?"
+
+    },
+
+    "MANAGE_USER_GROUP" : {
+
+        "ACTION_ACKNOWLEDGE"   : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"        : "@:APP.ACTION_CANCEL",
+        "ACTION_CLONE"         : "@:APP.ACTION_CLONE",
+        "ACTION_DELETE"        : "@:APP.ACTION_DELETE",
+        "ACTION_SAVE"          : "@:APP.ACTION_SAVE",
+
+        "DIALOG_HEADER_CONFIRM_DELETE" : "グループの削除",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_HEADER_ADMINISTER_SYSTEM"             : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM",
+        "FIELD_HEADER_CHANGE_OWN_PASSWORD"           : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD",
+        "FIELD_HEADER_CREATE_NEW_USERS"              : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS",
+        "FIELD_HEADER_CREATE_NEW_USER_GROUPS"        : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS",
+        "FIELD_HEADER_CREATE_NEW_CONNECTIONS"        : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS",
+        "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS"  : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS",
+        "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES"   : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES",
+        "FIELD_HEADER_USER_GROUP_NAME"               : "グループ名:",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_NO_USER_GROUPS"        : "このグループは現在どのグループにも属していません。グループを追加するにはこのセクションを展開してください。",
+        "HELP_NO_MEMBER_USER_GROUPS" : "このグループには現在グループが含まれていません。このセクションを展開してグループを追加してください。",
+        "HELP_NO_MEMBER_USERS"       : "このグループには現在ユーザーが含まれていません。ユーザーを追加するにはこのセクションを展開してください。",
+
+        "INFO_READ_ONLY"                : "このグループは編集できません",
+        "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE",
+        "INFO_NO_USERS_AVAILABLE"       : "利用可能なユーザがいません。",
+
+        "SECTION_HEADER_ALL_CONNECTIONS"     : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS",
+        "SECTION_HEADER_CONNECTIONS"         : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS",
+        "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS",
+        "SECTION_HEADER_EDIT_USER_GROUP"     : "グループ編集",
+        "SECTION_HEADER_MEMBER_USERS"        : "メンバーユーザ",
+        "SECTION_HEADER_MEMBER_USER_GROUPS"  : "メンバーグループ",
+        "SECTION_HEADER_PERMISSIONS"         : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS",
+        "SECTION_HEADER_USER_GROUPS"         : "親グループ",
+
+        "TEXT_CONFIRM_DELETE" : "削除したグループは復元できません。このグループを削除してもよろしいですか?"
+
+    },
+    
+    "PROTOCOL_RDP" : {
+
+        "FIELD_HEADER_CLIENT_NAME"     : "クライアント名:",
+        "FIELD_HEADER_COLOR_DEPTH"     : "カラー深度:",
+        "FIELD_HEADER_CONSOLE"         : "管理者コンソール:",
+        "FIELD_HEADER_CONSOLE_AUDIO"   : "コンソールでの音声サポート:",
+        "FIELD_HEADER_CREATE_DRIVE_PATH" : "ドライブの自動作成:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "ログの保存ディレクトリを自動的に作成する:",
+        "FIELD_HEADER_DISABLE_AUDIO"   : "オーディオの無効化:",
+        "FIELD_HEADER_DISABLE_AUTH"    : "認証の無効化:",
+        "FIELD_HEADER_DISABLE_COPY"    : "リモートデスクトップからのコピーを無効化:",
+        "FIELD_HEADER_DISABLE_PASTE"   : "クライアントからの貼り付けを無効化:",
+        "FIELD_HEADER_DOMAIN"          : "ドメイン:",
+        "FIELD_HEADER_DPI"             : "解像度 (DPI):",
+        "FIELD_HEADER_DRIVE_NAME"      : "ドライブ名:",
+        "FIELD_HEADER_DRIVE_PATH"      : "ドライブパス:",
+        "FIELD_HEADER_ENABLE_AUDIO_INPUT"         : "入力オーディオ(マイク)の有効化:",
+        "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "デスクトップコンポジション(Aero)の有効化:",
+        "FIELD_HEADER_ENABLE_DRIVE"               : "ドライブの有効化:",
+        "FIELD_HEADER_ENABLE_FONT_SMOOTHING"      : "クリアタイプフォントの有効化:",
+        "FIELD_HEADER_ENABLE_FULL_WINDOW_DRAG"    : "フルウィンドウドラッグの有効化:",
+        "FIELD_HEADER_ENABLE_MENU_ANIMATIONS"     : "メニューアニメーションの有効化:",
+        "FIELD_HEADER_DISABLE_BITMAP_CACHING"     : "ビットマップキャッシュの無効化:",
+        "FIELD_HEADER_DISABLE_OFFSCREEN_CACHING"  : "オフスクリーンキャッシュの無効化:",
+        "FIELD_HEADER_DISABLE_GLYPH_CACHING"      : "グリフキャッシュの無効化:",
+        "FIELD_HEADER_ENABLE_PRINTING"            : "印刷の有効化:",
+        "FIELD_HEADER_ENABLE_SFTP"     : "SFTPの有効化:",
+        "FIELD_HEADER_ENABLE_THEMING"             : "テーマの有効化:",
+        "FIELD_HEADER_ENABLE_WALLPAPER"           : "壁紙の有効化:",
+        "FIELD_HEADER_GATEWAY_DOMAIN"   : "ドメイン:",
+        "FIELD_HEADER_GATEWAY_HOSTNAME" : "ホスト名:",
+        "FIELD_HEADER_GATEWAY_PASSWORD" : "パスワード:",
+        "FIELD_HEADER_GATEWAY_PORT"     : "ポート:",
+        "FIELD_HEADER_GATEWAY_USERNAME" : "ユーザ名:",
+        "FIELD_HEADER_HEIGHT"          : "高さ:",
+        "FIELD_HEADER_HOSTNAME"        : "ホスト名:",
+        "FIELD_HEADER_IGNORE_CERT"     : "サーバ証明書を無視する:",
+        "FIELD_HEADER_INITIAL_PROGRAM" : "初期化プログラム:",
+        "FIELD_HEADER_LOAD_BALANCE_INFO" : "ロードバランス情報/クッキー:",
+        "FIELD_HEADER_PASSWORD"        : "パスワード:",
+        "FIELD_HEADER_PORT"            : "ポート:",
+        "FIELD_HEADER_PRINTER_NAME"    : "リダイレクトされたプリンタ名:",
+        "FIELD_HEADER_PRECONNECTION_BLOB" : "事前接続BLOB (VM ID):",
+        "FIELD_HEADER_PRECONNECTION_ID"   : "RDPソースID:",
+        "FIELD_HEADER_READ_ONLY"      : "読み取り専用:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "マウス動作の除外:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "画像/ストリームの除外:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "キーイベントの取得:",
+        "FIELD_HEADER_RECORDING_NAME" : "ログファイル名:",
+        "FIELD_HEADER_RECORDING_PATH" : "ログ保存ディレクトリ:",
+        "FIELD_HEADER_RESIZE_METHOD" : "サイズ変更方法:",
+        "FIELD_HEADER_REMOTE_APP_ARGS" : "パラメータ:",
+        "FIELD_HEADER_REMOTE_APP_DIR"  : "作業ディレクトリ:",
+        "FIELD_HEADER_REMOTE_APP"      : "プログラム:",
+        "FIELD_HEADER_SECURITY"        : "セキュリティモード:",
+        "FIELD_HEADER_SERVER_LAYOUT"   : "キーボードレイアウト:",
+        "FIELD_HEADER_SFTP_DIRECTORY"             : "デフォルトアップロードディレクトリ:",
+        "FIELD_HEADER_SFTP_HOST_KEY"              : "パブリックホストキー (Base64):",
+        "FIELD_HEADER_SFTP_HOSTNAME"              : "ホスト名:",
+        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "SFTPキープアライブ間隔:",
+        "FIELD_HEADER_SFTP_PASSPHRASE"            : "パスフレーズ:",
+        "FIELD_HEADER_SFTP_PASSWORD"              : "パスワード:",
+        "FIELD_HEADER_SFTP_PORT"                  : "ポート:",
+        "FIELD_HEADER_SFTP_PRIVATE_KEY"           : "秘密鍵:",
+        "FIELD_HEADER_SFTP_ROOT_DIRECTORY"        : "ファイルブラウザのルートディレクトリ:",
+        "FIELD_HEADER_SFTP_USERNAME"              : "ユーザ名:",
+        "FIELD_HEADER_STATIC_CHANNELS" : "静的チャンネル名:",
+        "FIELD_HEADER_USERNAME"        : "ユーザ名:",
+        "FIELD_HEADER_WIDTH"           : "幅:",
+
+        "FIELD_OPTION_COLOR_DEPTH_16"    : "Lowカラー (16ビット)",
+        "FIELD_OPTION_COLOR_DEPTH_24"    : "Trueカラー (24ビット)",
+        "FIELD_OPTION_COLOR_DEPTH_32"    : "Trueカラー (32ビット)",
+        "FIELD_OPTION_COLOR_DEPTH_8"     : "256カラー",
+
+        "FIELD_OPTION_RESIZE_METHOD_DISPLAY_UPDATE" : "\"ディスプレイアップデート\" 仮想チャンネル (RDP 8.1+)",
+        "FIELD_OPTION_RESIZE_METHOD_RECONNECT"      : "再接続",
+
+        "SECTION_HEADER_AUTHENTICATION"     : "認証",
+        "SECTION_HEADER_BASIC_PARAMETERS"   : "基本設定",
+        "SECTION_HEADER_DEVICE_REDIRECTION" : "デバイスリダイレクト",
+        "SECTION_HEADER_DISPLAY"            : "ディスプレイ",
+        "SECTION_HEADER_GATEWAY"            : "リモートデスクトップゲートウェイ",
+        "SECTION_HEADER_LOAD_BALANCING"     : "ロードバラシング",
+        "SECTION_HEADER_NETWORK"            : "ネットワーク",
+        "SECTION_HEADER_PERFORMANCE"        : "パフォーマンス",
+        "SECTION_HEADER_RECORDING"          : "スクリーンレコーディング",
+        "SECTION_HEADER_REMOTEAPP"          : "リモートアプリケーション"
+
+    },
+
+    "PROTOCOL_SSH" : {
+
+        "FIELD_HEADER_BACKSPACE"    : "Backspaceキーの送信:",
+        "FIELD_HEADER_COLOR_SCHEME" : "カラースキーマ:",
+        "FIELD_HEADER_COMMAND"      : "コマンドの実行:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "ログの保存ディレクトリを自動的に作成する:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "タイプスクリプトの保存ディレクトリを自動的に作成する:",
+        "FIELD_HEADER_DISABLE_COPY"  : "端末からのコピーを無効化:",
+        "FIELD_HEADER_DISABLE_PASTE" : "クライアントからの貼り付けを無効化:",
+        "FIELD_HEADER_FONT_NAME"     : "フォント名:",
+        "FIELD_HEADER_FONT_SIZE"     : "フォントサイズ:",
+        "FIELD_HEADER_ENABLE_SFTP"   : "SFTPの有効化:",
+        "FIELD_HEADER_HOST_KEY"      : "公開鍵(Base64):",
+        "FIELD_HEADER_HOSTNAME"      : "ホスト名:",
+        "FIELD_HEADER_USERNAME"      : "ユーザ名:",
+        "FIELD_HEADER_PASSWORD"      : "パスワード:",
+        "FIELD_HEADER_PASSPHRASE"    : "パスフレーズ:",
+        "FIELD_HEADER_PORT"          : "ポート:",
+        "FIELD_HEADER_PRIVATE_KEY"   : "秘密鍵:",
+        "FIELD_HEADER_READ_ONLY"     : "読み取り専用:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "マウス動作の除外:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "画像/ストリームの除外:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "キーイベントの取得:",
+        "FIELD_HEADER_RECORDING_NAME" : "ログファイル名:",
+        "FIELD_HEADER_RECORDING_PATH" : "ログ保存ディレクトリ",
+        "FIELD_HEADER_SERVER_ALIVE_INTERVAL" : "キープアライブ間隔:",
+        "FIELD_HEADER_SFTP_ROOT_DIRECTORY"   : "ファイルブラウザのルートディレクトリ:",
+        "FIELD_HEADER_TERMINAL_TYPE"   : "ターミナルタイプ:",
+        "FIELD_HEADER_TYPESCRIPT_NAME" : "タイプスクリプト名:",
+        "FIELD_HEADER_TYPESCRIPT_PATH" : "タイプスクリプトの保存ディレクトリ:",
+
+        "SECTION_HEADER_AUTHENTICATION" : "認証",
+        "SECTION_HEADER_BEHAVIOR"       : "ターミナルのふるまい",
+        "SECTION_HEADER_DISPLAY"        : "ディスプレイ",
+        "SECTION_HEADER_NETWORK"        : "ネットワーク",
+        "SECTION_HEADER_RECORDING"      : "スクリーンレコーディング",
+        "SECTION_HEADER_SESSION"        : "セッション / 環境",
+        "SECTION_HEADER_TYPESCRIPT"     : "タイプスクリプト (テキストの記録)"
+
+    },
+
+    "PROTOCOL_TELNET" : {
+
+        "FIELD_HEADER_BACKSPACE"      : "Backspaceキーの送信:",
+        "FIELD_HEADER_COLOR_SCHEME"   : "カラースキーマ:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "ログの保存ディレクトリを自動的に作成する:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "タイプスクリプトの保存ディレクトリを自動的に作成する:",
+        "FIELD_HEADER_DISABLE_COPY"  : "端末からのコピーを無効化:",
+        "FIELD_HEADER_DISABLE_PASTE" : "クライアントからの貼り付けを無効化:",
+        "FIELD_HEADER_FONT_NAME"      : "フォント名:",
+        "FIELD_HEADER_FONT_SIZE"      : "フォントサイズ:",
+        "FIELD_HEADER_HOSTNAME"       : "ホスト名:",
+        "FIELD_HEADER_LOGIN_FAILURE_REGEX" : "ログイン失敗正規表現:",
+        "FIELD_HEADER_LOGIN_SUCCESS_REGEX" : "ログイン成功正規表現:",
+        "FIELD_HEADER_USERNAME"       : "ユーザ名:",
+        "FIELD_HEADER_USERNAME_REGEX" : "ユーザ名正規表現:",
+        "FIELD_HEADER_PASSWORD"       : "パスワード:",
+        "FIELD_HEADER_PASSWORD_REGEX" : "パスワード正規表現:",
+        "FIELD_HEADER_PORT"           : "ポート:",
+        "FIELD_HEADER_READ_ONLY"      : "読み取り専用:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "マウス動作の除外:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "画像/ストリームの除外:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "キーイベントの取得:",
+        "FIELD_HEADER_RECORDING_NAME" : "ログファイル名:",
+        "FIELD_HEADER_RECORDING_PATH" : "ログ保存ディレクトリ:",
+        "FIELD_HEADER_TERMINAL_TYPE"   : "ターミナルタイプ:",
+        "FIELD_HEADER_TYPESCRIPT_NAME" : "タイプスクリプト名:",
+        "FIELD_HEADER_TYPESCRIPT_PATH" : "タイプスクリプト保存ディレクトリ:",
+
+        "SECTION_HEADER_AUTHENTICATION" : "認証",
+        "SECTION_HEADER_BEHAVIOR"       : "ターミナルのふるまい",
+        "SECTION_HEADER_DISPLAY"        : "ディスプレイ",
+        "SECTION_HEADER_RECORDING"      : "スクリーンレコーディング",
+        "SECTION_HEADER_TYPESCRIPT"     : "タイプスクリプト (テキストの記録)",
+        "SECTION_HEADER_NETWORK"        : "ネットワーク"
+
+    },
+
+    "PROTOCOL_VNC" : {
+
+        "FIELD_HEADER_AUDIO_SERVERNAME" : "オーディオサーバ名:",
+        "FIELD_HEADER_CLIPBOARD_ENCODING" : "エンコード:",
+        "FIELD_HEADER_COLOR_DEPTH"      : "カラー深度:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "ログの保存ディレクトリを自動的に作成する::",
+        "FIELD_HEADER_CURSOR"           : "カーソル:",
+        "FIELD_HEADER_DEST_HOST"        : "宛先ホスト:",
+        "FIELD_HEADER_DEST_PORT"        : "宛先ポート:",
+        "FIELD_HEADER_ENABLE_AUDIO"     : "オーディオの有効化:",
+        "FIELD_HEADER_ENABLE_SFTP"      : "SFTPの有効化:",
+        "FIELD_HEADER_HOSTNAME"         : "ホスト名:",
+        "FIELD_HEADER_PASSWORD"         : "パスワード:",
+        "FIELD_HEADER_PORT"             : "ポート:",
+        "FIELD_HEADER_READ_ONLY"        : "読み取り専用:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE"  : "マウス動作の除外:",
+        "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "画像/ストリームの除外:",
+        "FIELD_HEADER_RECORDING_INCLUDE_KEYS"   : "キーイベントの取得:",
+        "FIELD_HEADER_RECORDING_NAME" : "ログファイル名:",
+        "FIELD_HEADER_RECORDING_PATH" : "ログ保存ディレクトリ:",
+        "FIELD_HEADER_SFTP_DIRECTORY"             : "デフォルトアップロードディレクトリ:",
+        "FIELD_HEADER_SFTP_HOST_KEY"              : "公開鍵 (Base64):",
+        "FIELD_HEADER_SFTP_HOSTNAME"              : "ホスト名:",
+        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "SFTPキープアライブ間隔:",
+        "FIELD_HEADER_SFTP_PASSPHRASE"            : "パスフレーズ:",
+        "FIELD_HEADER_SFTP_PASSWORD"              : "パスワード:",
+        "FIELD_HEADER_SFTP_PORT"                  : "ポート:",
+        "FIELD_HEADER_SFTP_PRIVATE_KEY"           : "秘密鍵:",
+        "FIELD_HEADER_SFTP_ROOT_DIRECTORY"        : "ファイルブラウザのルートディレクトリ:",
+        "FIELD_HEADER_SFTP_USERNAME"              : "ユーザ名:",
+        "FIELD_HEADER_SWAP_RED_BLUE"    : "レッド・ブルー コンポーネントスワップ:",
+
+        "FIELD_OPTION_COLOR_DEPTH_8"     : "256カラー",
+        "FIELD_OPTION_COLOR_DEPTH_16"    : "Lowカラー (16ビット)",
+        "FIELD_OPTION_COLOR_DEPTH_24"    : "Trueカラー (24ビット)",
+        "FIELD_OPTION_COLOR_DEPTH_32"    : "Trueカラー (32ビット)",
+
+        "FIELD_OPTION_CURSOR_LOCAL"  : "ローカル",
+        "FIELD_OPTION_CURSOR_REMOTE" : "リモート",
+
+        "SECTION_HEADER_AUDIO"          : "オーティオ",
+        "SECTION_HEADER_AUTHENTICATION" : "認証",
+        "SECTION_HEADER_CLIPBOARD"      : "クリップボード",
+        "SECTION_HEADER_DISPLAY"        : "ディスプレイ",
+        "SECTION_HEADER_NETWORK"        : "ネットワーク",
+        "SECTION_HEADER_RECORDING"      : "スクリーンレコーディング",
+        "SECTION_HEADER_REPEATER"       : "VNCリピーター"
+
+    },
+
+    "SETTINGS" : {
+
+        "SECTION_HEADER_SETTINGS" : "設定"
+
+    },
+
+    "SETTINGS_CONNECTION_HISTORY" : {
+
+        "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD",
+        "ACTION_SEARCH"   : "@:APP.ACTION_SEARCH",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_CONNECTION_HISTORY" : "過去の接続履歴はここに表示されています。列の見出しをクリックしてソートすることができます。特定のレコードを検索するにはフィルタに検索キーワードを入力して、検索ボタンをクリックしてください。",
+
+        "INFO_CONNECTION_DURATION_UNKNOWN" : "--",
+        "INFO_NO_HISTORY"                  : "一致するレコードがありません。",
+
+        "TABLE_HEADER_SESSION_CONNECTION_NAME" : "接続名",
+        "TABLE_HEADER_SESSION_DURATION"        : "期間",
+        "TABLE_HEADER_SESSION_REMOTEHOST"      : "接続元",
+        "TABLE_HEADER_SESSION_STARTDATE"       : "開始時間",
+        "TABLE_HEADER_SESSION_USERNAME"        : "ユーザ名",
+
+        "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION"
+
+    },
+
+    "SETTINGS_CONNECTIONS" : {
+
+        "ACTION_ACKNOWLEDGE"          : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_NEW_CONNECTION"       : "接続の追加",
+        "ACTION_NEW_CONNECTION_GROUP" : "グループの追加",
+        "ACTION_NEW_SHARING_PROFILE"  : "共有プロファイルの追加",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_CONNECTIONS"   : "接続をクリックまたはタップすることで、管理画面が表示されます。権限に応じて接続のプロパティが変更できます。",
+        
+        "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT",
+
+        "SECTION_HEADER_CONNECTIONS"     : "接続"
+
+    },
+
+    "SETTINGS_PREFERENCES" : {
+
+        "ACTION_ACKNOWLEDGE"        : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"             : "@:APP.ACTION_CANCEL",
+        "ACTION_UPDATE_PASSWORD"    : "@:APP.ACTION_UPDATE_PASSWORD",
+
+        "DIALOG_HEADER_ERROR"    : "@:APP.DIALOG_HEADER_ERROR",
+
+        "ERROR_PASSWORD_BLANK"    : "@:APP.ERROR_PASSWORD_BLANK",
+        "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH",
+
+        "FIELD_HEADER_LANGUAGE"           : "表示言語:",
+        "FIELD_HEADER_PASSWORD"           : "パスワード:",
+        "FIELD_HEADER_PASSWORD_OLD"       : "現在のパスワード:",
+        "FIELD_HEADER_PASSWORD_NEW"       : "新しいパスワード:",
+        "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "新しいパスワード(確認):",
+        "FIELD_HEADER_USERNAME"           : "ユーザ名:",
+        
+        "HELP_DEFAULT_INPUT_METHOD" : "デフォルトの入力メソッドは、Guacamoleがどのようにキーボード入力を受け取るかを設定します。この設定の変更は、モバイルデバイスまたはIMEを通して入力を行う際に必要です。",
+        "HELP_DEFAULT_MOUSE_MODE"   : "デフォルトのマウスエミュレーションモードは、タッチに関して新しい接続でリモートマウスがどのように動作するかを決定します。この設定は、Guacamoleメニュー内で接続ごとに上書きすることができます。",
+        "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_LOCALE"             : "Guacamoleno言語を変更するには、下の言語を選択してください。選択可能な言語は、インストールされている言語によって異なります。",
+        "HELP_MOUSE_MODE_ABSOLUTE"  : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE",
+        "HELP_MOUSE_MODE_RELATIVE"  : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE",
+        "HELP_UPDATE_PASSWORD"      : "パスワードを変更する場合は、下に現在のパスワードと新しいパスワードを入力して、[パスワードの更新]をクリックしてください。変更はすぐに有効になります。",
+
+        "INFO_PASSWORD_CHANGED" : "パスワードが変更されました。",
+
+        "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE",
+        "NAME_INPUT_METHOD_OSK"  : "@:CLIENT.NAME_INPUT_METHOD_OSK",
+        "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT",
+
+        "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "デフォルトの入力方法",
+        "SECTION_HEADER_DEFAULT_MOUSE_MODE"   : "デフォルトのマウスエミュレーションモード",
+        "SECTION_HEADER_UPDATE_PASSWORD"      : "パスワード変更"
+
+    },
+
+    "SETTINGS_USERS" : {
+
+        "ACTION_ACKNOWLEDGE"   : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_NEW_USER"      : "ユーザ追加",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_USERS" : "ユーザをクリックまたはタップすることで、ユーザを管理できます。権限に応じてユーザ情報の変更を行うことができます。",
+
+        "SECTION_HEADER_USERS"       : "ユーザ",
+
+        "TABLE_HEADER_FULL_NAME"   : "フルネーム",
+        "TABLE_HEADER_LAST_ACTIVE" : "最後にアクティブになった時期",
+        "TABLE_HEADER_ORGANIZATION" : "組織",
+        "TABLE_HEADER_USERNAME"    : "ユーザ名"
+
+    },
+
+    "SETTINGS_USER_GROUPS" : {
+
+        "ACTION_ACKNOWLEDGE"    : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_NEW_USER_GROUP" : "グループ追加",
+
+        "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_USER_GROUPS" : "グループをクリックまたはタップすることで、グループを管理できます。権限に応じてグループ情報の変更を行うことができます。",
+
+        "SECTION_HEADER_USER_GROUPS" : "グループ",
+
+        "TABLE_HEADER_USER_GROUP_NAME" : "グループ名"
+
+    },
+
+    "SETTINGS_SESSIONS" : {
+        
+        "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE",
+        "ACTION_CANCEL"      : "@:APP.ACTION_CANCEL",
+        "ACTION_DELETE"      : "強制切断",
+        
+        "DIALOG_HEADER_CONFIRM_DELETE" : "セッションの強制切断",
+        "DIALOG_HEADER_ERROR"          : "@:APP.DIALOG_HEADER_ERROR",
+        
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+        
+        "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
+
+        "HELP_SESSIONS" : "Guacamoleのアクティブなセッションが全て表示されています。 もしセッションを強制切断したい場合、 チェックボックスにチェックを入れて、強制切断ボタンをクリックしてください。",
+        
+        "INFO_NO_SESSIONS" : "アクティブセッションはありません",
+
+        "SECTION_HEADER_SESSIONS" : "アクティブセッション",
+        
+        "TABLE_HEADER_SESSION_CONNECTION_NAME" : "接続名",
+        "TABLE_HEADER_SESSION_REMOTEHOST"      : "接続元",
+        "TABLE_HEADER_SESSION_USERNAME"        : "ユーザ名",
+        
+        "TEXT_CONFIRM_DELETE" : "選択したすべてのセッションを強制終了しますか?これらのセッションを使用しているユーザーは直ちに切断されます。"
+
+    },
+
+    "USER_ATTRIBUTES" : {
+
+        "FIELD_HEADER_GUAC_EMAIL_ADDRESS"       : "メールアドレス:",
+        "FIELD_HEADER_GUAC_FULL_NAME"           : "名前:",
+        "FIELD_HEADER_GUAC_ORGANIZATION"        : "組織:",
+        "FIELD_HEADER_GUAC_ORGANIZATIONAL_ROLE" : "役職/役割:"
+
+    },
+
+    "USER_MENU" : {
+
+        "ACTION_LOGOUT"             : "@:APP.ACTION_LOGOUT",
+        "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS",
+        "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES",
+        "ACTION_MANAGE_SESSIONS"    : "@:APP.ACTION_MANAGE_SESSIONS",
+        "ACTION_MANAGE_SETTINGS"    : "@:APP.ACTION_MANAGE_SETTINGS",
+        "ACTION_MANAGE_USERS"       : "@:APP.ACTION_MANAGE_USERS",
+        "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS",
+        "ACTION_NAVIGATE_HOME"      : "@:APP.ACTION_NAVIGATE_HOME",
+        "ACTION_VIEW_HISTORY"       : "@:APP.ACTION_VIEW_HISTORY"
+
+    }
+
+}