GUACAMOLE-684: Merge changes giving tentative acceptance of credentials priority over complete refusal.

As described in the discussion surrounding the original pull request:

 * An extension throws `GuacamoleInsufficientCredentialsException`
   specifically to indicate tentative acceptance of the credentials
   passed thus far.
 * Just as such an extension that fully accepts credentials takes
   priority over an extension that refuses to accept the same, it makes
   sense to allow an extension that *tentatively* accepts those credentials
   to also take priority.

With the above perspective, authentication result priorities are as
follows, with ties broken by the inherent order of the auth providers:

 1. Acceptance (returning an `AuthenticedUser` instance).
 2. Tentative acceptance (throwing
    `GuacamoleInvalidCredentialsException`).
 3. Complete refusal (any other subclass of
    `GuacamoleCredentialsException`).
 4. Neither refusal nor acceptance (returning `null`).

See: https://github.com/apache/guacamole-client/pull/352
diff --git a/Dockerfile b/Dockerfile
index 7eefcc8..0943dc3 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -30,6 +30,10 @@
 # Use official maven image for the build
 FROM maven:3-jdk-8 AS builder
 
+# Use args to build radius auth extension such as
+# `--build-arg BUILD_PROFILE=lgpl-extensions`
+ARG BUILD_PROFILE
+
 # Build environment variables
 ENV \
     BUILD_DIR=/tmp/guacamole-docker-BUILD
@@ -41,7 +45,7 @@
 COPY . "$BUILD_DIR"
 
 # Run the build itself
-RUN /opt/guacamole/bin/build-guacamole.sh "$BUILD_DIR" /opt/guacamole
+RUN /opt/guacamole/bin/build-guacamole.sh "$BUILD_DIR" /opt/guacamole "$BUILD_PROFILE"
 
 # For the runtime image, we start with the official Tomcat distribution
 FROM tomcat:${TOMCAT_VERSION}-${TOMCAT_JRE}
diff --git a/NOTICE b/NOTICE
index 97e6130..39de3ec 100644
--- a/NOTICE
+++ b/NOTICE
@@ -1,5 +1,5 @@
 Apache Guacamole
-Copyright 2018 The Apache Software Foundation
+Copyright 2019 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/doc/guacamole-example/pom.xml b/doc/guacamole-example/pom.xml
index f2f56f3..bb2ea68 100644
--- a/doc/guacamole-example/pom.xml
+++ b/doc/guacamole-example/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-example</artifactId>
     <packaging>war</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-example</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -106,7 +106,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>compile</scope>
         </dependency>
 
@@ -114,7 +114,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common-js</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <type>zip</type>
             <scope>runtime</scope>
         </dependency>
diff --git a/doc/guacamole-playback-example/pom.xml b/doc/guacamole-playback-example/pom.xml
index 1b31374..f7055d3 100644
--- a/doc/guacamole-playback-example/pom.xml
+++ b/doc/guacamole-playback-example/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-playback-example</artifactId>
     <packaging>war</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-playback-example</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -88,7 +88,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common-js</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <type>zip</type>
             <scope>runtime</scope>
         </dependency>
diff --git a/extensions/guacamole-auth-cas/pom.xml b/extensions/guacamole-auth-cas/pom.xml
index ae8bd67..3afabda 100644
--- a/extensions/guacamole-auth-cas/pom.xml
+++ b/extensions/guacamole-auth-cas/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-cas</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-cas</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -210,7 +210,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
@@ -218,7 +218,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
@@ -234,6 +234,13 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        
+        <!-- Guava - Utility Library -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>27.0.1-jre</version>
+        </dependency>
 
         <!-- Guice -->
         <dependency>
@@ -254,6 +261,14 @@
             <version>2.5</version>
             <scope>provided</scope>
         </dependency>
+        
+        <!-- Jersey - JAX-RS Implementation -->
+        <dependency>
+            <groupId>javax.ws.rs</groupId>
+            <artifactId>jsr311-api</artifactId>
+            <version>1.1.1</version>
+            <scope>provided</scope>
+        </dependency>
 
     </dependencies>
 
diff --git a/extensions/guacamole-auth-cas/src/licenses/LICENSE b/extensions/guacamole-auth-cas/src/licenses/LICENSE
index 1be0f68..6edeee8 100644
--- a/extensions/guacamole-auth-cas/src/licenses/LICENSE
+++ b/extensions/guacamole-auth-cas/src/licenses/LICENSE
@@ -219,6 +219,7 @@
     License(s):
         Public Domain (bundled/aopalliance-1.0/LICENSE)
 
+
 Google Guice (https://github.com/google/guice)
 ----------------------------------------------
 
@@ -227,6 +228,16 @@
     License(s):
         Apache v2.0 (bundled/guice-3.0/COPYING)
 
+
+Guava: Google Core Libraries for Java (https://github.com/google/guava)
+-----------------------------------------------------------------------
+
+    Version: 27.0.1-jre
+    From: 'Google Inc.' (http://www.google.com/)
+    License(s):
+        Apache v2.0 (bundled/guava-27.0.1-jre/COPYING)
+
+
 JSR-330 / Dependency Injection for Java (http://code.google.com/p/atinject/)
 ----------------------------------------------------------------------------
 
@@ -234,3 +245,4 @@
     From: 'JSR-330 Expert Group' (https://jcp.org/en/jsr/detail?id=330)
     License(s):
         Apache v2.0 (bundled/javax.inject-1/LICENSE-2.0.txt)
+
diff --git a/extensions/guacamole-auth-cas/src/licenses/NOTICE b/extensions/guacamole-auth-cas/src/licenses/NOTICE
index 97e6130..39de3ec 100644
--- a/extensions/guacamole-auth-cas/src/licenses/NOTICE
+++ b/extensions/guacamole-auth-cas/src/licenses/NOTICE
@@ -1,5 +1,5 @@
 Apache Guacamole
-Copyright 2018 The Apache Software Foundation
+Copyright 2019 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/extensions/guacamole-auth-totp/src/licenses/bundled/guava-18.0/COPYING b/extensions/guacamole-auth-cas/src/licenses/bundled/guava-27.0.1-jre/COPYING
similarity index 100%
copy from extensions/guacamole-auth-totp/src/licenses/bundled/guava-18.0/COPYING
copy to extensions/guacamole-auth-cas/src/licenses/bundled/guava-27.0.1-jre/COPYING
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/conf/CASGuacamoleProperties.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/CASGuacamoleProperties.java
index dd741a3..2ee42db 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/CASGuacamoleProperties.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/CASGuacamoleProperties.java
@@ -19,7 +19,7 @@
 
 package org.apache.guacamole.auth.cas.conf;
 
-import org.apache.guacamole.properties.StringGuacamoleProperty;
+import org.apache.guacamole.properties.URIGuacamoleProperty;
 
 /**
  * Provides properties required for use of the CAS authentication provider.
@@ -36,8 +36,8 @@
     /**
      * The authorization endpoint (URI) of the CAS service.
      */
-    public static final StringGuacamoleProperty CAS_AUTHORIZATION_ENDPOINT =
-            new StringGuacamoleProperty() {
+    public static final URIGuacamoleProperty CAS_AUTHORIZATION_ENDPOINT =
+            new URIGuacamoleProperty() {
 
         @Override
         public String getName() { return "cas-authorization-endpoint"; }
@@ -49,8 +49,8 @@
      * authentication process is complete. This must be the full URL that a
      * user would enter into their browser to access Guacamole.
      */
-    public static final StringGuacamoleProperty CAS_REDIRECT_URI =
-            new StringGuacamoleProperty() {
+    public static final URIGuacamoleProperty CAS_REDIRECT_URI =
+            new URIGuacamoleProperty() {
 
         @Override
         public String getName() { return "cas-redirect-uri"; }
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java
index e0016ad..680f170 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/conf/ConfigurationService.java
@@ -20,6 +20,7 @@
 package org.apache.guacamole.auth.cas.conf;
 
 import com.google.inject.Inject;
+import java.net.URI;
 import java.security.PrivateKey;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.environment.Environment;
@@ -47,7 +48,7 @@
      *     If guacamole.properties cannot be parsed, or if the authorization
      *     endpoint property is missing.
      */
-    public String getAuthorizationEndpoint() throws GuacamoleException {
+    public URI getAuthorizationEndpoint() throws GuacamoleException {
         return environment.getRequiredProperty(CASGuacamoleProperties.CAS_AUTHORIZATION_ENDPOINT);
     }
 
@@ -65,7 +66,7 @@
      *     If guacamole.properties cannot be parsed, or if the redirect URI
      *     property is missing.
      */
-    public String getRedirectURI() throws GuacamoleException {
+    public URI getRedirectURI() throws GuacamoleException {
         return environment.getRequiredProperty(CASGuacamoleProperties.CAS_REDIRECT_URI);
     }
 
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/form/CASTicketField.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/form/CASTicketField.java
index f785241..c16f525 100644
--- a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/form/CASTicketField.java
+++ b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/form/CASTicketField.java
@@ -19,8 +19,8 @@
 
 package org.apache.guacamole.auth.cas.form;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
+import java.net.URI;
+import javax.ws.rs.core.UriBuilder;
 import org.apache.guacamole.form.Field;
 
 
@@ -47,7 +47,7 @@
     /**
      * The full URI which the field should link to.
      */
-    private final String authorizationURI;
+    private final URI authorizationURI;
 
     /**
      * Creates a new CAS "ticket" field which links to the given CAS
@@ -65,29 +65,15 @@
      *     The URI that the CAS service should redirect to upon successful
      *     authentication.
      */
-    public CASTicketField(String authorizationEndpoint, String redirectURI) {
+    public CASTicketField(URI authorizationEndpoint, URI redirectURI) {
 
         // Init base field properties
         super(PARAMETER_NAME, "GUAC_CAS_TICKET");
-
-        // Build authorization URI from given values
-        try {
-            final StringBuilder sb = new StringBuilder();
-            sb.append(authorizationEndpoint);
-            // user might configure the endpoint with a trailing slash
-            if (sb.charAt(sb.length() - 1) != '/') {
-                sb.append('/');
-            }
-            sb.append(CAS_LOGIN_URI);
-            sb.append("?service=");
-            sb.append(URLEncoder.encode(redirectURI, "UTF-8"));
-            this.authorizationURI = sb.toString();
-        }
-
-        // Java is required to provide UTF-8 support
-        catch (UnsupportedEncodingException e) {
-            throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
-        }
+        
+        this.authorizationURI = UriBuilder.fromUri(authorizationEndpoint)
+                .path(CAS_LOGIN_URI)
+                .queryParam("service", redirectURI)
+                .build();
 
     }
 
@@ -99,7 +85,7 @@
      *     The full URI that this field should link to.
      */
     public String getAuthorizationURI() {
-        return authorizationURI;
+        return authorizationURI.toString();
     }
 
 }
diff --git a/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java b/extensions/guacamole-auth-cas/src/main/java/org/apache/guacamole/auth/cas/ticket/TicketValidationService.java
index bfc3b69..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
@@ -19,7 +19,9 @@
 
 package org.apache.guacamole.auth.cas.ticket;
 
+import com.google.common.io.BaseEncoding;
 import com.google.inject.Inject;
+import java.net.URI;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
@@ -28,13 +30,15 @@
 import javax.crypto.IllegalBlockSizeException;
 import javax.crypto.NoSuchPaddingException;
 import java.nio.charset.Charset;
-import javax.xml.bind.DatatypeConverter;
+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;
@@ -52,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.
@@ -60,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.
@@ -72,49 +81,61 @@
      *     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,
         // grab the principal returned by the validator.
-        String casServerUrl = confService.getAuthorizationEndpoint();
-        Cas20ProxyTicketValidator validator = new Cas20ProxyTicketValidator(casServerUrl);
+        URI casServerUrl = confService.getAuthorizationEndpoint();
+        Cas20ProxyTicketValidator validator = new Cas20ProxyTicketValidator(casServerUrl.toString());
         validator.setAcceptAnyProxy(true);
         validator.setEncoding("UTF-8");
         try {
-            String confRedirectURI = confService.getRedirectURI();
-            Assertion a = validator.validate(ticket, confRedirectURI);
+            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);
-        }
 
     }
 
@@ -161,7 +182,7 @@
             cipher.init(Cipher.DECRYPT_MODE, clearpassKey);
 
             // Decode and decrypt, and return a new string.
-            final byte[] pass64 = DatatypeConverter.parseBase64Binary(encryptedPassword);
+            final byte[] pass64 = BaseEncoding.base64().decode(encryptedPassword);
             final byte[] cipherData = cipher.doFinal(pass64);
             return new String(cipherData, Charset.forName("UTF-8"));
 
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 fbf8c09..40d0334 100644
--- a/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "CAS Authentication Extension",
     "namespace" : "guac-cas",
@@ -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/pom.xml b/extensions/guacamole-auth-duo/pom.xml
index 55de752..8de36e0 100644
--- a/extensions/guacamole-auth-duo/pom.xml
+++ b/extensions/guacamole-auth-duo/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-duo</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-duo</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -213,9 +213,16 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
+        
+        <!-- Guava - Utility Library -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>27.0.1-jre</version>
+        </dependency>
 
         <!-- Guice -->
         <dependency>
diff --git a/extensions/guacamole-auth-duo/src/licenses/LICENSE b/extensions/guacamole-auth-duo/src/licenses/LICENSE
index 4ae92c4..b357f34 100644
--- a/extensions/guacamole-auth-duo/src/licenses/LICENSE
+++ b/extensions/guacamole-auth-duo/src/licenses/LICENSE
@@ -264,6 +264,15 @@
         Apache v2.0 (bundled/guice-3.0/COPYING)
 
 
+Guava: Google Core Libraries for Java (https://github.com/google/guava)
+-----------------------------------------------------------------------
+
+    Version: 27.0.1-jre
+    From: 'Google Inc.' (http://www.google.com/)
+    License(s):
+        Apache v2.0 (bundled/guava-27.0.1-jre/COPYING)
+
+
 JSR-330 / Dependency Injection for Java (http://code.google.com/p/atinject/)
 ----------------------------------------------------------------------------
 
diff --git a/extensions/guacamole-auth-duo/src/licenses/NOTICE b/extensions/guacamole-auth-duo/src/licenses/NOTICE
index 97e6130..39de3ec 100644
--- a/extensions/guacamole-auth-duo/src/licenses/NOTICE
+++ b/extensions/guacamole-auth-duo/src/licenses/NOTICE
@@ -1,5 +1,5 @@
 Apache Guacamole
-Copyright 2018 The Apache Software Foundation
+Copyright 2019 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/extensions/guacamole-auth-totp/src/licenses/bundled/guava-18.0/COPYING b/extensions/guacamole-auth-duo/src/licenses/bundled/guava-27.0.1-jre/COPYING
similarity index 100%
copy from extensions/guacamole-auth-totp/src/licenses/bundled/guava-18.0/COPYING
copy to extensions/guacamole-auth-duo/src/licenses/bundled/guava-27.0.1-jre/COPYING
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java
index 1de9a67..6fa2a88 100644
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/DuoCookie.java
@@ -19,10 +19,10 @@
 
 package org.apache.guacamole.auth.duo.api;
 
+import com.google.common.io.BaseEncoding;
 import java.io.UnsupportedEncodingException;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
-import javax.xml.bind.DatatypeConverter;
 import org.apache.guacamole.GuacamoleClientException;
 import org.apache.guacamole.GuacamoleException;
 
@@ -171,7 +171,7 @@
         // Attempt to decode data as base64
         String data;
         try {
-            data = new String(DatatypeConverter.parseBase64Binary(str), "UTF-8");
+            data = new String(BaseEncoding.base64().decode(str), "UTF-8");
         }
 
         // Bail if invalid base64 is provided
@@ -231,7 +231,7 @@
             String data = username + "|" + integrationKey + "|" + expires;
 
             // Encode resulting cookie string with base64
-            return DatatypeConverter.printBase64Binary(data.getBytes("UTF-8"));
+            return BaseEncoding.base64().encode(data.getBytes("UTF-8"));
 
         }
 
diff --git a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java
index 49fb34b..c959acd 100644
--- a/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java
+++ b/extensions/guacamole-auth-duo/src/main/java/org/apache/guacamole/auth/duo/api/SignedDuoCookie.java
@@ -19,6 +19,7 @@
 
 package org.apache.guacamole.auth.duo.api;
 
+import com.google.common.io.BaseEncoding;
 import java.io.UnsupportedEncodingException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
@@ -26,7 +27,6 @@
 import java.util.regex.Pattern;
 import javax.crypto.Mac;
 import javax.crypto.spec.SecretKeySpec;
-import javax.xml.bind.DatatypeConverter;
 import org.apache.guacamole.GuacamoleClientException;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
@@ -221,7 +221,7 @@
             mac.init(new SecretKeySpec(key.getBytes("UTF-8"), SIGNATURE_ALGORITHM));
 
             // Return signature as hex
-            return DatatypeConverter.printHexBinary(mac.doFinal(data.getBytes("UTF-8"))).toLowerCase();
+            return BaseEncoding.base16().lowerCase().encode(mac.doFinal(data.getBytes("UTF-8")));
 
         }
 
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 4c87382..7daefdd 100644
--- a/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-duo/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "Duo TFA Authentication Backend",
     "namespace" : "duo",
@@ -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-header/pom.xml b/extensions/guacamole-auth-header/pom.xml
index 9c07bab..cf65658 100644
--- a/extensions/guacamole-auth-header/pom.xml
+++ b/extensions/guacamole-auth-header/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-header</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-header</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -130,7 +130,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-header/src/licenses/NOTICE b/extensions/guacamole-auth-header/src/licenses/NOTICE
index 97e6130..39de3ec 100644
--- a/extensions/guacamole-auth-header/src/licenses/NOTICE
+++ b/extensions/guacamole-auth-header/src/licenses/NOTICE
@@ -1,5 +1,5 @@
 Apache Guacamole
-Copyright 2018 The Apache Software Foundation
+Copyright 2019 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/extensions/guacamole-auth-header/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-header/src/main/resources/guac-manifest.json
index c976eef..3c71028 100644
--- a/extensions/guacamole-auth-header/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-header/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "HTTP Header Authentication Extension",
     "namespace" : "guac-header",
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
index a3b7f92..23c374b 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.0.0</version>
+        <version>1.1.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -135,7 +135,7 @@
         <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
-            <version>19.0</version>
+            <version>27.0.1-jre</version>
         </dependency>
 
     </dependencies>
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/ActiveConnectionPermissionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.java
index e7cbd5d..1e52571 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/activeconnection/ActiveConnectionPermissionService.java
@@ -96,8 +96,8 @@
                 String identifier = record.getUUID().toString();
                 permissions.add(new ObjectPermission(ObjectPermission.Type.READ, identifier));
 
-                // If we're and admin, then we also have DELETE
-                if (isAdmin)
+                // If we're an admin, or the connection is ours, then we can DELETE
+                if (isAdmin || targetEntity.isUser(record.getUsername()))
                     permissions.add(new ObjectPermission(ObjectPermission.Type.DELETE, identifier));
 
             }
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 5e459b1..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
@@ -34,6 +34,8 @@
 import org.apache.guacamole.auth.jdbc.tunnel.GuacamoleTunnelService;
 import org.apache.guacamole.net.GuacamoleTunnel;
 import org.apache.guacamole.net.auth.ActiveConnection;
+import org.apache.guacamole.net.auth.permission.ObjectPermission;
+import org.apache.guacamole.net.auth.permission.ObjectPermissionSet;
 
 /**
  * Service which provides convenience methods for creating, retrieving, and
@@ -89,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);
             }
 
@@ -111,13 +114,12 @@
     public void deleteObject(ModeledAuthenticatedUser user, String identifier)
         throws GuacamoleException {
 
-        // Only administrators may delete active connections
-        if (!user.getUser().isAdministrator())
-            throw new GuacamoleSecurityException("Permission denied.");
-
-        // Close connection, if it exists (and we have permission)
+        // Close connection, if it exists and we have permission
         ActiveConnection activeConnection = retrieveObject(user, identifier);
-        if (activeConnection != null) {
+        if (activeConnection == null)
+            return;
+        
+        if (hasObjectPermissions(user, identifier, ObjectPermission.Type.DELETE)) {
 
             // Close connection if not already closed
             GuacamoleTunnel tunnel = activeConnection.getTunnel();
@@ -125,6 +127,8 @@
                 tunnel.close();
 
         }
+        else
+            throw new GuacamoleSecurityException("Permission denied.");
         
     }
 
@@ -162,4 +166,54 @@
 
     }
 
+    /**
+     * Retrieve the permission set for the specified user that relates
+     * to access to active connections.
+     * 
+     * @param user
+     *     The user for which to retrieve the permission set.
+     * 
+     * @return
+     *     A permission set associated with the given user that specifies
+     *     the permissions available for active connection objects.
+     * 
+     * @throws GuacamoleException
+     *     If permission to read permissions for the user is denied.
+     */
+    private ObjectPermissionSet getPermissionSet(ModeledAuthenticatedUser user) 
+            throws GuacamoleException {
+        return user.getUser().getActiveConnectionPermissions();
+    }
+
+    /**
+     * Return a boolean value representing whether or not a user has the given
+     * permission available to them on the active connection with the given
+     * identifier.
+     * 
+     * @param user
+     *     The user for which the permissions are being queried.
+     * 
+     * @param identifier
+     *     The identifier of the active connection we are wondering about.
+     * 
+     * @param type
+     *     The type of permission being requested.
+     * 
+     * @return
+     *     True if the user has the necessary permission; otherwise false.
+     * 
+     * @throws GuacamoleException 
+     *     If the user does not have access to read permissions.
+     */
+    private boolean hasObjectPermissions(ModeledAuthenticatedUser user,
+            String identifier, ObjectPermission.Type type)
+            throws GuacamoleException {
+        
+        ObjectPermissionSet permissionSet = getPermissionSet(user);
+        
+        return user.getUser().isAdministrator() 
+                || permissionSet.hasPermission(type, identifier);
+        
+    }
+
 }
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/base/ModeledPermissions.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledPermissions.java
index cda6f6a..965062c 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledPermissions.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/base/ModeledPermissions.java
@@ -106,6 +106,21 @@
     }
 
     /**
+     * Returns whether the underlying entity represents a specific user having
+     * the given username.
+     *
+     * @param username
+     *     The username of a user.
+     *
+     * @return
+     *     true if the underlying entity is a user that has the given username,
+     *     false otherwise.
+     */
+    public boolean isUser(String username) {
+        return isUser() && getIdentifier().equals(username);
+    }
+
+    /**
      * Returns whether the underlying entity is a user group. Entities may be
      * either users or user groups.
      *
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java
index 8dcf6f5..e2f3c15 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ConnectionService.java
@@ -499,6 +499,10 @@
      * @param info
      *     Information associated with the connecting client.
      *
+     * @param tokens
+     *     A Map containing the token names and corresponding values to be
+     *     applied as parameter tokens when establishing the connection.
+     *
      * @return
      *     A connected GuacamoleTunnel associated with a newly-established
      *     connection.
@@ -507,12 +511,12 @@
      *     If permission to connect to this connection is denied.
      */
     public GuacamoleTunnel connect(ModeledAuthenticatedUser user,
-            ModeledConnection connection, GuacamoleClientInformation info)
-            throws GuacamoleException {
+            ModeledConnection connection, GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
 
         // Connect only if READ permission is granted
         if (hasObjectPermission(user, connection.getIdentifier(), ObjectPermission.Type.READ))
-            return tunnelService.getGuacamoleTunnel(user, connection, info);
+            return tunnelService.getGuacamoleTunnel(user, connection, info, tokens);
 
         // The user does not have permission to connect
         throw new GuacamoleSecurityException("Permission denied.");
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java
index 660212c..b492626 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connection/ModeledConnection.java
@@ -259,8 +259,9 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException {
-        return connectionService.connect(getCurrentUser(), this, info);
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+        return connectionService.connect(getCurrentUser(), this, info, tokens);
     }
 
     @Override
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java
index 01119b9..3e9ec72 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ConnectionGroupService.java
@@ -21,6 +21,7 @@
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import java.util.Map;
 import java.util.Set;
 import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
 import org.apache.guacamole.auth.jdbc.base.ModeledDirectoryObjectMapper;
@@ -243,6 +244,10 @@
      * @param info
      *     Information associated with the connecting client.
      *
+     * @param tokens
+     *     A Map containing the token names and corresponding values to be
+     *     applied as parameter tokens when establishing the connection.
+     *
      * @return
      *     A connected GuacamoleTunnel associated with a newly-established
      *     connection.
@@ -251,12 +256,12 @@
      *     If permission to connect to this connection is denied.
      */
     public GuacamoleTunnel connect(ModeledAuthenticatedUser user,
-            ModeledConnectionGroup connectionGroup, GuacamoleClientInformation info)
-            throws GuacamoleException {
+            ModeledConnectionGroup connectionGroup, GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
 
         // Connect only if READ permission is granted
         if (hasObjectPermission(user, connectionGroup.getIdentifier(), ObjectPermission.Type.READ))
-            return tunnelService.getGuacamoleTunnel(user, connectionGroup, info);
+            return tunnelService.getGuacamoleTunnel(user, connectionGroup, info, tokens);
 
         // The user does not have permission to connect
         throw new GuacamoleSecurityException("Permission denied.");
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java
index 3aac52d..bcf457a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/ModeledConnectionGroup.java
@@ -135,9 +135,9 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info)
-            throws GuacamoleException {
-        return connectionGroupService.connect(getCurrentUser(), this, info);
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+        return connectionGroupService.connect(getCurrentUser(), this, info, tokens);
     }
 
     @Override
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/RootConnectionGroup.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/RootConnectionGroup.java
index d2e5551..08b32fd 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/RootConnectionGroup.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/connectiongroup/RootConnectionGroup.java
@@ -122,8 +122,8 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info)
-            throws GuacamoleException {
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
         throw new GuacamoleSecurityException("Permission denied.");
     }
 
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/AbstractPermissionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/AbstractPermissionService.java
index 6e4ddfa..eea570f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/AbstractPermissionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/permission/AbstractPermissionService.java
@@ -101,7 +101,7 @@
             throws GuacamoleException {
 
         // A user can always read their own permissions
-        if (targetEntity.isUser() && user.getUser().getIdentifier().equals(targetEntity.getIdentifier()))
+        if (targetEntity.isUser(user.getUser().getIdentifier()))
             return true;
         
         // A system adminstrator can do anything
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/SHA256PasswordEncryptionService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/SHA256PasswordEncryptionService.java
index ebcd1cd..83055d9 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/SHA256PasswordEncryptionService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/security/SHA256PasswordEncryptionService.java
@@ -19,10 +19,10 @@
 
 package org.apache.guacamole.auth.jdbc.security;
 
+import com.google.common.io.BaseEncoding;
 import java.io.UnsupportedEncodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
-import javax.xml.bind.DatatypeConverter;
 
 /**
  * Provides a SHA-256 based implementation of the password encryption
@@ -40,7 +40,7 @@
             builder.append(password);
 
             if (salt != null)
-                builder.append(DatatypeConverter.printHexBinary(salt));
+                builder.append(BaseEncoding.base16().encode(salt));
 
             // Hash UTF-8 bytes of possibly-salted password
             MessageDigest md = MessageDigest.getInstance("SHA-256");
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/SharedConnection.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnection.java
index 5483d02..cf00831 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnection.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connection/SharedConnection.java
@@ -131,9 +131,9 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info)
-            throws GuacamoleException {
-        return tunnelService.getGuacamoleTunnel(user, definition, info);
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+        return tunnelService.getGuacamoleTunnel(user, definition, info, tokens);
     }
 
     @Override
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/sharing/connectiongroup/SharedRootConnectionGroup.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connectiongroup/SharedRootConnectionGroup.java
index 71b997c..33d9ca7 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connectiongroup/SharedRootConnectionGroup.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/sharing/connectiongroup/SharedRootConnectionGroup.java
@@ -98,8 +98,8 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info)
-            throws GuacamoleException {
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
         throw new GuacamoleSecurityException("Permission denied.");
     }
 
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 5f7fc1b..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
@@ -52,7 +52,6 @@
 import org.apache.guacamole.protocol.ConfiguredGuacamoleSocket;
 import org.apache.guacamole.protocol.GuacamoleClientInformation;
 import org.apache.guacamole.protocol.GuacamoleConfiguration;
-import org.apache.guacamole.token.StandardTokens;
 import org.apache.guacamole.token.TokenFilter;
 import org.mybatis.guice.transactional.Transactional;
 import org.apache.guacamole.auth.jdbc.connection.ConnectionParameterMapper;
@@ -203,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
@@ -214,32 +215,35 @@
      *     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());
         for (ConnectionParameterModel parameter : parameters)
             config.setParameter(parameter.getName(), parameter.getValue());
 
-        // Build token filter containing credential tokens
-        TokenFilter tokenFilter = new TokenFilter();
-        StandardTokens.addStandardTokens(tokenFilter, user);
-
-        // Filter the configuration
-        tokenFilter.filterValues(config.getParameters());
-
         return config;
         
     }
@@ -279,13 +283,6 @@
         for (SharingProfileParameterModel parameter : parameters)
             config.setParameter(parameter.getName(), parameter.getValue());
 
-        // Build token filter containing credential tokens
-        TokenFilter tokenFilter = new TokenFilter();
-        StandardTokens.addStandardTokens(tokenFilter, user);
-
-        // Filter the configuration
-        tokenFilter.filterValues(config.getParameters());
-
         return config;
 
     }
@@ -454,6 +451,10 @@
      *     Information describing the Guacamole client connecting to the given
      *     connection.
      *
+     * @param tokens
+     *     A Map containing the token names and corresponding values to be
+     *     applied as parameter tokens when establishing the connection.
+     *
      * @param interceptErrors
      *     Whether errors from the upstream remote desktop should be
      *     intercepted and rethrown as GuacamoleUpstreamExceptions.
@@ -467,7 +468,8 @@
      *     while connection configuration information is being retrieved.
      */
     private GuacamoleTunnel assignGuacamoleTunnel(ActiveConnectionRecord activeConnection,
-            GuacamoleClientInformation info, boolean interceptErrors) throws GuacamoleException {
+            GuacamoleClientInformation info, Map<String, String> tokens,
+            boolean interceptErrors) throws GuacamoleException {
 
         // Record new active connection
         Runnable cleanupTask = new ConnectionCleanupTask(activeConnection);
@@ -480,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
@@ -504,6 +507,13 @@
 
             }
 
+            // Build token filter containing credential tokens
+            TokenFilter tokenFilter = new TokenFilter();
+            tokenFilter.setTokens(tokens);
+
+            // Filter the configuration
+            tokenFilter.filterValues(config.getParameters());
+
             // Obtain socket which will automatically run the cleanup task
             ConfiguredGuacamoleSocket socket = new ConfiguredGuacamoleSocket(
                 getUnconfiguredGuacamoleSocket(connection.getGuacamoleProxyConfiguration(),
@@ -651,8 +661,8 @@
     @Override
     @Transactional
     public GuacamoleTunnel getGuacamoleTunnel(final ModeledAuthenticatedUser user,
-            final ModeledConnection connection, GuacamoleClientInformation info)
-            throws GuacamoleException {
+            final ModeledConnection connection, GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
 
         // Acquire access to single connection, ignoring the failover-only flag
         acquire(user, Collections.singletonList(connection), true);
@@ -660,7 +670,7 @@
         // Connect only if the connection was successfully acquired
         ActiveConnectionRecord connectionRecord = activeConnectionRecordProvider.get();
         connectionRecord.init(user, connection);
-        return assignGuacamoleTunnel(connectionRecord, info, false);
+        return assignGuacamoleTunnel(connectionRecord, info, tokens, false);
 
     }
 
@@ -673,7 +683,8 @@
     @Transactional
     public GuacamoleTunnel getGuacamoleTunnel(ModeledAuthenticatedUser user,
             ModeledConnectionGroup connectionGroup,
-            GuacamoleClientInformation info) throws GuacamoleException {
+            GuacamoleClientInformation info, Map<String, String> tokens)
+            throws GuacamoleException {
 
         // Track failures in upstream (remote desktop) connections
         boolean upstreamHasFailed = false;
@@ -706,7 +717,8 @@
                 // Connect to acquired child
                 ActiveConnectionRecord connectionRecord = activeConnectionRecordProvider.get();
                 connectionRecord.init(user, connectionGroup, connection);
-                GuacamoleTunnel tunnel = assignGuacamoleTunnel(connectionRecord, info, connections.size() > 1);
+                GuacamoleTunnel tunnel = assignGuacamoleTunnel(connectionRecord,
+                        info, tokens, connections.size() > 1);
 
                 // If session affinity is enabled, prefer this connection going forward
                 if (connectionGroup.isSessionAffinityEnabled())
@@ -755,7 +767,7 @@
     @Transactional
     public GuacamoleTunnel getGuacamoleTunnel(RemoteAuthenticatedUser user,
             SharedConnectionDefinition definition,
-            GuacamoleClientInformation info)
+            GuacamoleClientInformation info, Map<String, String> tokens)
             throws GuacamoleException {
 
         // Create a connection record which describes the shared connection
@@ -764,7 +776,7 @@
                 definition.getSharingProfile());
 
         // Connect to shared connection described by the created record
-        GuacamoleTunnel tunnel = assignGuacamoleTunnel(connectionRecord, info, false);
+        GuacamoleTunnel tunnel = assignGuacamoleTunnel(connectionRecord, info, tokens, false);
 
         // Register tunnel, such that it is closed when the
         // SharedConnectionDefinition is invalidated
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/tunnel/GuacamoleTunnelService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/GuacamoleTunnelService.java
index 34d9293..bad5219 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/GuacamoleTunnelService.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/tunnel/GuacamoleTunnelService.java
@@ -20,6 +20,7 @@
 package org.apache.guacamole.auth.jdbc.tunnel;
 
 import java.util.Collection;
+import java.util.Map;
 import org.apache.guacamole.auth.jdbc.user.ModeledAuthenticatedUser;
 import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
 import org.apache.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
@@ -73,6 +74,10 @@
      *     Information describing the Guacamole client connecting to the given
      *     connection.
      *
+     * @param tokens
+     *     A Map containing the token names and corresponding values to be
+     *     applied as parameter tokens when establishing the connection.
+     *
      * @return
      *     A new GuacamoleTunnel which is configured and connected to the given
      *     connection.
@@ -82,8 +87,8 @@
      *     rules.
      */
     GuacamoleTunnel getGuacamoleTunnel(ModeledAuthenticatedUser user,
-            ModeledConnection connection, GuacamoleClientInformation info)
-            throws GuacamoleException;
+            ModeledConnection connection, GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException;
 
     /**
      * Returns a collection containing connection records representing all
@@ -117,6 +122,10 @@
      *     Information describing the Guacamole client connecting to the given
      *     connection group.
      *
+     * @param tokens
+     *     A Map containing the token names and corresponding values to be
+     *     applied as parameter tokens when establishing the connection.
+     *
      * @return
      *     A new GuacamoleTunnel which is configured and connected to the given
      *     connection group.
@@ -127,7 +136,7 @@
      */
     GuacamoleTunnel getGuacamoleTunnel(ModeledAuthenticatedUser user,
             ModeledConnectionGroup connectionGroup,
-            GuacamoleClientInformation info)
+            GuacamoleClientInformation info, Map<String, String> tokens)
             throws GuacamoleException;
 
     /**
@@ -163,6 +172,10 @@
      *     Information describing the Guacamole client connecting to the given
      *     connection.
      *
+     * @param tokens
+     *     A Map containing the token names and corresponding values to be
+     *     applied as parameter tokens when establishing the connection.
+     *
      * @return
      *     A new GuacamoleTunnel which is configured and connected to the given
      *     active connection.
@@ -173,7 +186,7 @@
      */
     GuacamoleTunnel getGuacamoleTunnel(RemoteAuthenticatedUser user,
             SharedConnectionDefinition definition,
-            GuacamoleClientInformation info)
+            GuacamoleClientInformation info, Map<String, String> tokens)
             throws GuacamoleException;
 
 }
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 e756374..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;
@@ -93,7 +94,7 @@
      *     A ModeledUser object which is backed by the data associated with
      *     this user in the database.
      *
-     * @param credentials 
+     * @param credentials
      *     The credentials given by the user when they authenticated.
      */
     public ModeledAuthenticatedUser(AuthenticationProvider authenticationProvider,
@@ -107,7 +108,7 @@
      * Returns a ModeledUser object which is backed by the data associated with
      * this user within the database.
      *
-     * @return 
+     * @return
      *     A ModeledUser object which is backed by the data associated with
      *     this user in the database.
      */
@@ -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/RemoteAuthenticatedUser.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/RemoteAuthenticatedUser.java
index 324892e..a936e4e 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/RemoteAuthenticatedUser.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/apache/guacamole/auth/jdbc/user/RemoteAuthenticatedUser.java
@@ -58,7 +58,7 @@
      * @param authenticationProvider
      *     The AuthenticationProvider that has authenticated the given user.
      *
-     * @param credentials 
+     * @param credentials
      *     The credentials given by the user when they authenticated.
      *
      * @param effectiveGroups
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-dist/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/pom.xml
index a204c8e..ddb57a9 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.0.0</version>
+        <version>1.1.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -99,21 +99,21 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-mysql</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
         <!-- PostgreSQL Authentication Extension -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-postgresql</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
         <!-- SQL Server Authentication Extension -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-sqlserver</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/LICENSE b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/LICENSE
index 4bf8148..7d27dd1 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/LICENSE
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/LICENSE
@@ -232,10 +232,10 @@
 Guava: Google Core Libraries for Java (https://github.com/google/guava)
 -----------------------------------------------------------------------
 
-    Version: 18.0
+    Version: 27.0.1-jre
     From: 'Google Inc.' (http://www.google.com/)
     License(s):
-        Apache v2.0 (bundled/guava-18.0/COPYING)
+        Apache v2.0 (bundled/guava-27.0.1-jre/COPYING)
 
 
 JSR-330 / Dependency Injection for Java (http://code.google.com/p/atinject/)
@@ -263,4 +263,3 @@
     From: 'MyBatis' (http://www.mybatis.org/)
     License(s):
         Apache v2.0 (bundled/mybatis-guice-3.6/LICENSE)
-
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/NOTICE b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/NOTICE
index 97e6130..39de3ec 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/NOTICE
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/NOTICE
@@ -1,5 +1,5 @@
 Apache Guacamole
-Copyright 2018 The Apache Software Foundation
+Copyright 2019 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/bundled/guava-18.0/COPYING b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/bundled/guava-27.0.1-jre/COPYING
similarity index 100%
rename from extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/bundled/guava-18.0/COPYING
rename to extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/bundled/guava-27.0.1-jre/COPYING
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/bundled/jaxb-api-2.3.1/license.txt b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/bundled/jaxb-api-2.3.1/license.txt
new file mode 100644
index 0000000..4b156e6
--- /dev/null
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/bundled/jaxb-api-2.3.1/license.txt
@@ -0,0 +1,760 @@
+COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL) Version 1.1
+
+1. Definitions.
+
+    1.1. "Contributor" means each individual or entity that creates or
+    contributes to the creation of Modifications.
+
+    1.2. "Contributor Version" means the combination of the Original
+    Software, prior Modifications used by a Contributor (if any), and
+    the Modifications made by that particular Contributor.
+
+    1.3. "Covered Software" means (a) the Original Software, or (b)
+    Modifications, or (c) the combination of files containing Original
+    Software with files containing Modifications, in each case including
+    portions thereof.
+
+    1.4. "Executable" means the Covered Software in any form other than
+    Source Code.
+
+    1.5. "Initial Developer" means the individual or entity that first
+    makes Original Software available under this License.
+
+    1.6. "Larger Work" means a work which combines Covered Software or
+    portions thereof with code not governed by the terms of this License.
+
+    1.7. "License" means this document.
+
+    1.8. "Licensable" means having the right to grant, to the maximum
+    extent possible, whether at the time of the initial grant or
+    subsequently acquired, any and all of the rights conveyed herein.
+
+    1.9. "Modifications" means the Source Code and Executable form of
+    any of the following:
+
+    A. Any file that results from an addition to, deletion from or
+    modification of the contents of a file containing Original Software
+    or previous Modifications;
+
+    B. Any new file that contains any part of the Original Software or
+    previous Modification; or
+
+    C. Any new file that is contributed or otherwise made available
+    under the terms of this License.
+
+    1.10. "Original Software" means the Source Code and Executable form
+    of computer software code that is originally released under this
+    License.
+
+    1.11. "Patent Claims" means any patent claim(s), now owned or
+    hereafter acquired, including without limitation, method, process,
+    and apparatus claims, in any patent Licensable by grantor.
+
+    1.12. "Source Code" means (a) the common form of computer software
+    code in which modifications are made and (b) associated
+    documentation included in or with such code.
+
+    1.13. "You" (or "Your") means an individual or a legal entity
+    exercising rights under, and complying with all of the terms of,
+    this License. For legal entities, "You" includes any entity which
+    controls, is controlled by, or is under common control with You. For
+    purposes of this definition, "control" means (a) the power, direct
+    or indirect, to cause the direction or management of such entity,
+    whether by contract or otherwise, or (b) ownership of more than
+    fifty percent (50%) of the outstanding shares or beneficial
+    ownership of such entity.
+
+2. License Grants.
+
+    2.1. The Initial Developer Grant.
+
+    Conditioned upon Your compliance with Section 3.1 below and subject
+    to third party intellectual property claims, the Initial Developer
+    hereby grants You a world-wide, royalty-free, non-exclusive license:
+
+    (a) under intellectual property rights (other than patent or
+    trademark) Licensable by Initial Developer, to use, reproduce,
+    modify, display, perform, sublicense and distribute the Original
+    Software (or portions thereof), with or without Modifications,
+    and/or as part of a Larger Work; and
+
+    (b) under Patent Claims infringed by the making, using or selling of
+    Original Software, to make, have made, use, practice, sell, and
+    offer for sale, and/or otherwise dispose of the Original Software
+    (or portions thereof).
+
+    (c) The licenses granted in Sections 2.1(a) and (b) are effective on
+    the date Initial Developer first distributes or otherwise makes the
+    Original Software available to a third party under the terms of this
+    License.
+
+    (d) Notwithstanding Section 2.1(b) above, no patent license is
+    granted: (1) for code that You delete from the Original Software, or
+    (2) for infringements caused by: (i) the modification of the
+    Original Software, or (ii) the combination of the Original Software
+    with other software or devices.
+
+    2.2. Contributor Grant.
+
+    Conditioned upon Your compliance with Section 3.1 below and subject
+    to third party intellectual property claims, each Contributor hereby
+    grants You a world-wide, royalty-free, non-exclusive license:
+
+    (a) under intellectual property rights (other than patent or
+    trademark) Licensable by Contributor to use, reproduce, modify,
+    display, perform, sublicense and distribute the Modifications
+    created by such Contributor (or portions thereof), either on an
+    unmodified basis, with other Modifications, as Covered Software
+    and/or as part of a Larger Work; and
+
+    (b) under Patent Claims infringed by the making, using, or selling
+    of Modifications made by that Contributor either alone and/or in
+    combination with its Contributor Version (or portions of such
+    combination), to make, use, sell, offer for sale, have made, and/or
+    otherwise dispose of: (1) Modifications made by that Contributor (or
+    portions thereof); and (2) the combination of Modifications made by
+    that Contributor with its Contributor Version (or portions of such
+    combination).
+
+    (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective
+    on the date Contributor first distributes or otherwise makes the
+    Modifications available to a third party.
+
+    (d) Notwithstanding Section 2.2(b) above, no patent license is
+    granted: (1) for any code that Contributor has deleted from the
+    Contributor Version; (2) for infringements caused by: (i) third
+    party modifications of Contributor Version, or (ii) the combination
+    of Modifications made by that Contributor with other software
+    (except as part of the Contributor Version) or other devices; or (3)
+    under Patent Claims infringed by Covered Software in the absence of
+    Modifications made by that Contributor.
+
+3. Distribution Obligations.
+
+    3.1. Availability of Source Code.
+
+    Any Covered Software that You distribute or otherwise make available
+    in Executable form must also be made available in Source Code form
+    and that Source Code form must be distributed only under the terms
+    of this License. You must include a copy of this License with every
+    copy of the Source Code form of the Covered Software You distribute
+    or otherwise make available. You must inform recipients of any such
+    Covered Software in Executable form as to how they can obtain such
+    Covered Software in Source Code form in a reasonable manner on or
+    through a medium customarily used for software exchange.
+
+    3.2. Modifications.
+
+    The Modifications that You create or to which You contribute are
+    governed by the terms of this License. You represent that You
+    believe Your Modifications are Your original creation(s) and/or You
+    have sufficient rights to grant the rights conveyed by this License.
+
+    3.3. Required Notices.
+
+    You must include a notice in each of Your Modifications that
+    identifies You as the Contributor of the Modification. You may not
+    remove or alter any copyright, patent or trademark notices contained
+    within the Covered Software, or any notices of licensing or any
+    descriptive text giving attribution to any Contributor or the
+    Initial Developer.
+
+    3.4. Application of Additional Terms.
+
+    You may not offer or impose any terms on any Covered Software in
+    Source Code form that alters or restricts the applicable version of
+    this License or the recipients' rights hereunder. You may choose to
+    offer, and to charge a fee for, warranty, support, indemnity or
+    liability obligations to one or more recipients of Covered Software.
+    However, you may do so only on Your own behalf, and not on behalf of
+    the Initial Developer or any Contributor. You must make it
+    absolutely clear that any such warranty, support, indemnity or
+    liability obligation is offered by You alone, and You hereby agree
+    to indemnify the Initial Developer and every Contributor for any
+    liability incurred by the Initial Developer or such Contributor as a
+    result of warranty, support, indemnity or liability terms You offer.
+
+    3.5. Distribution of Executable Versions.
+
+    You may distribute the Executable form of the Covered Software under
+    the terms of this License or under the terms of a license of Your
+    choice, which may contain terms different from this License,
+    provided that You are in compliance with the terms of this License
+    and that the license for the Executable form does not attempt to
+    limit or alter the recipient's rights in the Source Code form from
+    the rights set forth in this License. If You distribute the Covered
+    Software in Executable form under a different license, You must make
+    it absolutely clear that any terms which differ from this License
+    are offered by You alone, not by the Initial Developer or
+    Contributor. You hereby agree to indemnify the Initial Developer and
+    every Contributor for any liability incurred by the Initial
+    Developer or such Contributor as a result of any such terms You offer.
+
+    3.6. Larger Works.
+
+    You may create a Larger Work by combining Covered Software with
+    other code not governed by the terms of this License and distribute
+    the Larger Work as a single product. In such a case, You must make
+    sure the requirements of this License are fulfilled for the Covered
+    Software.
+
+4. Versions of the License.
+
+    4.1. New Versions.
+
+    Oracle is the initial license steward and may publish revised and/or
+    new versions of this License from time to time. Each version will be
+    given a distinguishing version number. Except as provided in Section
+    4.3, no one other than the license steward has the right to modify
+    this License.
+
+    4.2. Effect of New Versions.
+
+    You may always continue to use, distribute or otherwise make the
+    Covered Software available under the terms of the version of the
+    License under which You originally received the Covered Software. If
+    the Initial Developer includes a notice in the Original Software
+    prohibiting it from being distributed or otherwise made available
+    under any subsequent version of the License, You must distribute and
+    make the Covered Software available under the terms of the version
+    of the License under which You originally received the Covered
+    Software. Otherwise, You may also choose to use, distribute or
+    otherwise make the Covered Software available under the terms of any
+    subsequent version of the License published by the license steward.
+
+    4.3. Modified Versions.
+
+    When You are an Initial Developer and You want to create a new
+    license for Your Original Software, You may create and use a
+    modified version of this License if You: (a) rename the license and
+    remove any references to the name of the license steward (except to
+    note that the license differs from this License); and (b) otherwise
+    make it clear that the license contains terms which differ from this
+    License.
+
+5. DISCLAIMER OF WARRANTY.
+
+    COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS,
+    WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED,
+    INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE
+    IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR
+    NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF
+    THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE
+    DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY
+    OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING,
+    REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN
+    ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS
+    AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
+
+6. TERMINATION.
+
+    6.1. This License and the rights granted hereunder will terminate
+    automatically if You fail to comply with terms herein and fail to
+    cure such breach within 30 days of becoming aware of the breach.
+    Provisions which, by their nature, must remain in effect beyond the
+    termination of this License shall survive.
+
+    6.2. If You assert a patent infringement claim (excluding
+    declaratory judgment actions) against Initial Developer or a
+    Contributor (the Initial Developer or Contributor against whom You
+    assert such claim is referred to as "Participant") alleging that the
+    Participant Software (meaning the Contributor Version where the
+    Participant is a Contributor or the Original Software where the
+    Participant is the Initial Developer) directly or indirectly
+    infringes any patent, then any and all rights granted directly or
+    indirectly to You by such Participant, the Initial Developer (if the
+    Initial Developer is not the Participant) and all Contributors under
+    Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice
+    from Participant terminate prospectively and automatically at the
+    expiration of such 60 day notice period, unless if within such 60
+    day period You withdraw Your claim with respect to the Participant
+    Software against such Participant either unilaterally or pursuant to
+    a written agreement with Participant.
+
+    6.3. If You assert a patent infringement claim against Participant
+    alleging that the Participant Software directly or indirectly
+    infringes any patent where such claim is resolved (such as by
+    license or settlement) prior to the initiation of patent
+    infringement litigation, then the reasonable value of the licenses
+    granted by such Participant under Sections 2.1 or 2.2 shall be taken
+    into account in determining the amount or value of any payment or
+    license.
+
+    6.4. In the event of termination under Sections 6.1 or 6.2 above,
+    all end user licenses that have been validly granted by You or any
+    distributor hereunder prior to termination (excluding licenses
+    granted to You by any distributor) shall survive termination.
+
+7. LIMITATION OF LIABILITY.
+
+    UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT
+    (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE
+    INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF
+    COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE
+    TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR
+    CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT
+    LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER
+    FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR
+    LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE
+    POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT
+    APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH
+    PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH
+    LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR
+    LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION
+    AND LIMITATION MAY NOT APPLY TO YOU.
+
+8. U.S. GOVERNMENT END USERS.
+
+    The Covered Software is a "commercial item," as that term is defined
+    in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer
+    software" (as that term is defined at 48 C.F.R. §
+    252.227-7014(a)(1)) and "commercial computer software documentation"
+    as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent
+    with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4
+    (June 1995), all U.S. Government End Users acquire Covered Software
+    with only those rights set forth herein. This U.S. Government Rights
+    clause is in lieu of, and supersedes, any other FAR, DFAR, or other
+    clause or provision that addresses Government rights in computer
+    software under this License.
+
+9. MISCELLANEOUS.
+
+    This License represents the complete agreement concerning subject
+    matter hereof. If any provision of this License is held to be
+    unenforceable, such provision shall be reformed only to the extent
+    necessary to make it enforceable. This License shall be governed by
+    the law of the jurisdiction specified in a notice contained within
+    the Original Software (except to the extent applicable law, if any,
+    provides otherwise), excluding such jurisdiction's conflict-of-law
+    provisions. Any litigation relating to this License shall be subject
+    to the jurisdiction of the courts located in the jurisdiction and
+    venue specified in a notice contained within the Original Software,
+    with the losing party responsible for costs, including, without
+    limitation, court costs and reasonable attorneys' fees and expenses.
+    The application of the United Nations Convention on Contracts for
+    the International Sale of Goods is expressly excluded. Any law or
+    regulation which provides that the language of a contract shall be
+    construed against the drafter shall not apply to this License. You
+    agree that You alone are responsible for compliance with the United
+    States export administration regulations (and the export control
+    laws and regulation of any other countries) when You use, distribute
+    or otherwise make available any Covered Software.
+
+10. RESPONSIBILITY FOR CLAIMS.
+
+    As between Initial Developer and the Contributors, each party is
+    responsible for claims and damages arising, directly or indirectly,
+    out of its utilization of rights under this License and You agree to
+    work with Initial Developer and Contributors to distribute such
+    responsibility on an equitable basis. Nothing herein is intended or
+    shall be deemed to constitute any admission of liability.
+
+------------------------------------------------------------------------
+
+NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION
+LICENSE (CDDL)
+
+The code released under the CDDL shall be governed by the laws of the
+State of California (excluding conflict-of-law provisions). Any
+litigation relating to this License shall be subject to the jurisdiction
+of the Federal Courts of the Northern District of California and the
+state courts of the State of California, with venue lying in Santa Clara
+County, California.
+
+
+
+  The GNU General Public License (GPL) Version 2, June 1991
+
+Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+51 Franklin Street, Fifth Floor
+Boston, MA 02110-1335
+USA
+
+Everyone is permitted to copy and distribute verbatim copies
+of this license document, but changing it is not allowed.
+
+Preamble
+
+The licenses for most software are designed to take away your freedom to
+share and change it. By contrast, the GNU General Public License is
+intended to guarantee your freedom to share and change free software--to
+make sure the software is free for all its users. This General Public
+License applies to most of the Free Software Foundation's software and
+to any other program whose authors commit to using it. (Some other Free
+Software Foundation software is covered by the GNU Library General
+Public License instead.) You can apply it to your programs, too.
+
+When we speak of free software, we are referring to freedom, not price.
+Our General Public Licenses are designed to make sure that you have the
+freedom to distribute copies of free software (and charge for this
+service if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs; and that you know you can do these things.
+
+To protect your rights, we need to make restrictions that forbid anyone
+to deny you these rights or to ask you to surrender the rights. These
+restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+For example, if you distribute copies of such a program, whether gratis
+or for a fee, you must give the recipients all the rights that you have.
+You must make sure that they, too, receive or can get the source code.
+And you must show them these terms so they know their rights.
+
+We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+Finally, any free program is threatened constantly by software patents.
+We wish to avoid the danger that redistributors of a free program will
+individually obtain patent licenses, in effect making the program
+proprietary. To prevent this, we have made it clear that any patent must
+be licensed for everyone's free use or not licensed at all.
+
+The precise terms and conditions for copying, distribution and
+modification follow.
+
+TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+0. This License applies to any program or other work which contains a
+notice placed by the copyright holder saying it may be distributed under
+the terms of this General Public License. The "Program", below, refers
+to any such program or work, and a "work based on the Program" means
+either the Program or any derivative work under copyright law: that is
+to say, a work containing the Program or a portion of it, either
+verbatim or with modifications and/or translated into another language.
+(Hereinafter, translation is included without limitation in the term
+"modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of running
+the Program is not restricted, and the output from the Program is
+covered only if its contents constitute a work based on the Program
+(independent of having been made by running the Program). Whether that
+is true depends on what the Program does.
+
+1. You may copy and distribute verbatim copies of the Program's source
+code as you receive it, in any medium, provided that you conspicuously
+and appropriately publish on each copy an appropriate copyright notice
+and disclaimer of warranty; keep intact all the notices that refer to
+this License and to the absence of any warranty; and give any other
+recipients of the Program a copy of this License along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+2. You may modify your copy or copies of the Program or any portion of
+it, thus forming a work based on the Program, and copy and distribute
+such modifications or work under the terms of Section 1 above, provided
+that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any part
+    thereof, to be licensed as a whole at no charge to all third parties
+    under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a notice
+    that there is no warranty (or else, saying that you provide a
+    warranty) and that users may redistribute the program under these
+    conditions, and telling the user how to view a copy of this License.
+    (Exception: if the Program itself is interactive but does not
+    normally print such an announcement, your work based on the Program
+    is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program, and
+can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based on
+the Program, the distribution of the whole must be on the terms of this
+License, whose permissions for other licensees extend to the entire
+whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of a
+storage or distribution medium does not bring the other work under the
+scope of this License.
+
+3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections 1
+    and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your cost
+    of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer to
+    distribute corresponding source code. (This alternative is allowed
+    only for noncommercial distribution and only if you received the
+    program in object code or executable form with such an offer, in
+    accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source code
+means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to control
+compilation and installation of the executable. However, as a special
+exception, the source code distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies the
+executable.
+
+If distribution of executable or object code is made by offering access
+to copy from a designated place, then offering equivalent access to copy
+the source code from the same place counts as distribution of the source
+code, even though third parties are not compelled to copy the source
+along with the object code.
+
+4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt otherwise
+to copy, modify, sublicense or distribute the Program is void, and will
+automatically terminate your rights under this License. However, parties
+who have received copies, or rights, from you under this License will
+not have their licenses terminated so long as such parties remain in
+full compliance.
+
+5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and all
+its terms and conditions for copying, distributing or modifying the
+Program or works based on it.
+
+6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further restrictions
+on the recipients' exercise of the rights granted herein. You are not
+responsible for enforcing compliance by third parties to this License.
+
+7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot distribute
+so as to satisfy simultaneously your obligations under this License and
+any other pertinent obligations, then as a consequence you may not
+distribute the Program at all. For example, if a patent license would
+not permit royalty-free redistribution of the Program by all those who
+receive copies directly or indirectly through you, then the only way you
+could satisfy both it and this License would be to refrain entirely from
+distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is implemented
+by public license practices. Many people have made generous
+contributions to the wide range of software distributed through that
+system in reliance on consistent application of that system; it is up to
+the author/donor to decide if he or she is willing to distribute
+software through any other system and a licensee cannot impose that choice.
+
+This section is intended to make thoroughly clear what is believed to be
+a consequence of the rest of this License.
+
+8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License may
+add an explicit geographical distribution limitation excluding those
+countries, so that distribution is permitted only in or among countries
+not thus excluded. In such case, this License incorporates the
+limitation as if written in the body of this License.
+
+9. The Free Software Foundation may publish revised and/or new
+versions of the General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Program does not specify a version
+number of this License, you may choose any version ever published by the
+Free Software Foundation.
+
+10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the
+author to ask for permission. For software which is copyrighted by the
+Free Software Foundation, write to the Free Software Foundation; we
+sometimes make exceptions for this. Our decision will be guided by the
+two goals of preserving the free status of all derivatives of our free
+software and of promoting the sharing and reuse of software generally.
+
+NO WARRANTY
+
+11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND,
+EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE
+ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH
+YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL
+NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR
+DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL
+DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM
+(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
+INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF
+THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR
+OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
+
+END OF TERMS AND CONDITIONS
+
+How to Apply These Terms to Your New Programs
+
+If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+To do so, attach the following notices to the program. It is safest to
+attach them to the start of each source file to most effectively convey
+the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+    One line to give the program's name and a brief idea of what it does.
+    Copyright (C) <year> <name of author>
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful, but
+    WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+    General Public License for more details.
+
+    You should have received a copy of the GNU General Public License
+    along with this program; if not, write to the Free Software
+    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type
+    `show w'. This is free software, and you are welcome to redistribute
+    it under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the
+appropriate parts of the General Public License. Of course, the commands
+you use may be called something other than `show w' and `show c'; they
+could even be mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+    Yoyodyne, Inc., hereby disclaims all copyright interest in the
+    program `Gnomovision' (which makes passes at compilers) written by
+    James Hacker.
+
+    signature of Ty Coon, 1 April 1989
+    Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program
+into proprietary programs. If your program is a subroutine library, you
+may consider it more useful to permit linking proprietary applications
+with the library. If this is what you want to do, use the GNU Library
+General Public License instead of this License.
+
+#
+
+Certain source files distributed by Oracle America, Inc. and/or its
+affiliates are subject to the following clarification and special
+exception to the GPLv2, based on the GNU Project exception for its
+Classpath libraries, known as the GNU Classpath Exception, but only
+where Oracle has expressly included in the particular source file's
+header the words "Oracle designates this particular file as subject to
+the "Classpath" exception as provided by Oracle in the LICENSE file
+that accompanied this code."
+
+You should also note that Oracle includes multiple, independent
+programs in this software package. Some of those programs are provided
+under licenses deemed incompatible with the GPLv2 by the Free Software
+Foundation and others.  For example, the package includes programs
+licensed under the Apache License, Version 2.0.  Such programs are
+licensed to you under their original licenses.
+
+Oracle facilitates your further distribution of this package by adding
+the Classpath Exception to the necessary parts of its GPLv2 code, which
+permits you to use that code in combination with other independent
+modules not licensed under the GPLv2.  However, note that this would
+not permit you to commingle code under an incompatible license with
+Oracle's GPLv2 licensed code by, for example, cutting and pasting such
+code into a file also containing Oracle's GPLv2 licensed code and then
+distributing the result.  Additionally, if you were to remove the
+Classpath Exception from any of the files to which it applies and
+distribute the result, you would likely be required to license some or
+all of the other code in that distribution under the GPLv2 as well, and
+since the GPLv2 is incompatible with the license terms of some items
+included in the distribution by Oracle, removing the Classpath
+Exception could therefore effectively compromise your ability to
+further distribute the package.
+
+Proceed with caution and we recommend that you obtain the advice of a
+lawyer skilled in open source matters before removing the Classpath
+Exception or making modifications to this package which may
+subsequently be redistributed and/or involve the use of third party
+software.
+
+CLASSPATH EXCEPTION
+Linking this library statically or dynamically with other modules is
+making a combined work based on this library.  Thus, the terms and
+conditions of the GNU General Public License version 2 cover the whole
+combination.
+
+As a special exception, the copyright holders of this library give you
+permission to link this library with independent modules to produce an
+executable, regardless of the license terms of these independent
+modules, and to copy and distribute the resulting executable under
+terms of your choice, provided that you also meet, for each linked
+independent module, the terms and conditions of the license of that
+module.  An independent module is a module which is not derived from or
+based on this library.  If you modify this library, you may extend this
+exception to your version of the library, but you are not obligated to
+do so.  If you do not wish to do so, delete this exception statement
+from your version.
+
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml
index b438735..8a7d68a 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.0.0</version>
+        <version>1.1.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -120,7 +120,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-base</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/guac-manifest.json
index 7fb613c..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
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "MySQL Authentication",
     "namespace" : "guac-mysql",
@@ -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-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
index adb9618..455f31f 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
@@ -51,7 +51,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="ConnectionGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_group_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
index 3b8afc7..862c5c7 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
@@ -51,7 +51,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="ConnectionPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
index a0b2872..bf8706e 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
@@ -51,7 +51,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="SharingProfilePermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             sharing_profile_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
index d8af2bc..ea76617 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
@@ -54,7 +54,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="UserGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
index 4470aa3..52c83e3 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
@@ -54,7 +54,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="UserPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml
index bfe4a3c..340eba7 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.0.0</version>
+        <version>1.1.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -120,7 +120,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-base</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/guac-manifest.json
index b4047bc..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
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "PostgreSQL Authentication",
     "namespace" : "guac-postgresql",
@@ -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-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
index 4ce168d..e3b4053 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
@@ -51,7 +51,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="ConnectionGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_group_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
index 68968d7..cd9cf11 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
@@ -51,7 +51,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="ConnectionPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
index 4594c05..d4ce589 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
@@ -51,7 +51,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="SharingProfilePermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             sharing_profile_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
index 44ac201..afa1035 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
@@ -54,7 +54,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="UserGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
index bd3ff93..fbd8ae0 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
@@ -54,7 +54,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="UserPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml
index 0a8468b..55455db 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/pom.xml
@@ -36,7 +36,7 @@
     <parent>
         <groupId>org.apache.guacamole</groupId>
         <artifactId>guacamole-auth-jdbc</artifactId>
-        <version>1.0.0</version>
+        <version>1.1.0</version>
         <relativePath>../../</relativePath>
     </parent>
 
@@ -120,7 +120,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-auth-jdbc-base</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
     </dependencies>
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/guac-manifest.json
index c7a0caa..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
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "SQLServer Authentication",
     "namespace" : "guac-sqlserver",
@@ -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/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
index b891868..ef58daf 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionGroupPermissionMapper.xml
@@ -51,7 +51,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="ConnectionGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_group_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
index acd02ab..882e6dd 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/ConnectionPermissionMapper.xml
@@ -51,7 +51,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="ConnectionPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             connection_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
index 7acc290..23feeee 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/SharingProfilePermissionMapper.xml
@@ -51,7 +51,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="SharingProfilePermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             sharing_profile_id
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
index 331a3a3..6dd0b5d 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserGroupPermissionMapper.xml
@@ -54,7 +54,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="UserGroupPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
index 53ed027..4503419 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-sqlserver/src/main/resources/org/apache/guacamole/auth/jdbc/permission/UserPermissionMapper.xml
@@ -54,7 +54,7 @@
     <!-- Select the single permission matching the given criteria -->
     <select id="selectOne" resultMap="UserPermissionResultMap">
 
-        SELECT
+        SELECT DISTINCT
             #{entity.entityID,jdbcType=INTEGER} AS entity_id,
             permission,
             affected_entity.name AS affected_name
diff --git a/extensions/guacamole-auth-jdbc/pom.xml b/extensions/guacamole-auth-jdbc/pom.xml
index 9aee2df..1325949 100644
--- a/extensions/guacamole-auth-jdbc/pom.xml
+++ b/extensions/guacamole-auth-jdbc/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-jdbc</artifactId>
     <packaging>pom</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-jdbc</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -81,7 +81,7 @@
             <dependency>
                 <groupId>org.apache.guacamole</groupId>
                 <artifactId>guacamole-ext</artifactId>
-                <version>1.0.0</version>
+                <version>1.1.0</version>
                 <scope>provided</scope>
             </dependency>
 
diff --git a/extensions/guacamole-auth-ldap/pom.xml b/extensions/guacamole-auth-ldap/pom.xml
index 22d59bd..17c8ed6 100644
--- a/extensions/guacamole-auth-ldap/pom.xml
+++ b/extensions/guacamole-auth-ldap/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-ldap</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-ldap</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -137,15 +137,21 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
-        <!-- JLDAP -->
+        <!-- Apache Directory LDAP API -->
         <dependency>
-            <groupId>com.novell.ldap</groupId>
-            <artifactId>jldap</artifactId>
-            <version>4.3</version>
+            <groupId>org.apache.directory.api</groupId>
+            <artifactId>api-all</artifactId>
+            <version>2.0.0.AM4</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.slf4j</groupId>
+                    <artifactId>slf4j-api</artifactId>
+                </exclusion>
+            </exclusions>
         </dependency>
 
         <!-- Guice -->
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/NOTICE b/extensions/guacamole-auth-ldap/src/licenses/NOTICE
index 97e6130..39de3ec 100644
--- a/extensions/guacamole-auth-ldap/src/licenses/NOTICE
+++ b/extensions/guacamole-auth-ldap/src/licenses/NOTICE
@@ -1,5 +1,5 @@
 Apache Guacamole
-Copyright 2018 The Apache Software Foundation
+Copyright 2019 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/extensions/guacamole-auth-totp/src/licenses/bundled/guava-18.0/COPYING b/extensions/guacamole-auth-ldap/src/licenses/bundled/directory-api-2.0.0/LICENSE-2.0.txt
similarity index 100%
copy from extensions/guacamole-auth-totp/src/licenses/bundled/guava-18.0/COPYING
copy to extensions/guacamole-auth-ldap/src/licenses/bundled/directory-api-2.0.0/LICENSE-2.0.txt
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 4a746f1..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,15 +21,27 @@
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.novell.ldap.LDAPConnection;
+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.guacamole.auth.ldap.user.AuthenticatedUser;
-import org.apache.guacamole.auth.ldap.user.UserContext;
+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;
@@ -44,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.
@@ -74,13 +91,13 @@
      * Provider for AuthenticatedUser objects.
      */
     @Inject
-    private Provider<AuthenticatedUser> authenticatedUserProvider;
+    private Provider<LDAPAuthenticatedUser> authenticatedUserProvider;
 
     /**
      * Provider for UserContext objects.
      */
     @Inject
-    private Provider<UserContext> userContextProvider;
+    private Provider<LDAPUserContext> userContextProvider;
 
     /**
      * Determines the DN which corresponds to the user having the given
@@ -98,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()
             );
@@ -121,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;
 
@@ -149,55 +165,9 @@
     }
 
     /**
-     * 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.
+     * given credentials. Also adds custom LDAP attributes to the
+     * AuthenticatedUser.
      *
      * @param credentials
      *     The credentials to use for authentication.
@@ -210,43 +180,105 @@
      *     If an error occurs while authenticating the user, or if access is
      *     denied.
      */
-    public AuthenticatedUser authenticateUser(Credentials credentials)
+    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);
+        // Return AuthenticatedUser if bind succeeds
+        LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
+        authenticatedUser.init(credentials, getAttributeTokens(ldapConnection,
+                bindDn), effectiveGroups, bindDn);
+        
+        return authenticatedUser;
 
+    }
+
+    /**
+     * Returns parameter tokens generated from LDAP attributes on the user
+     * currently bound under the given LDAP connection. The attributes to be
+     * converted into parameter tokens must be explicitly listed in
+     * guacamole.properties. If no attributes are specified or none are
+     * found on the LDAP user object, an empty map is returned.
+     *
+     * @param ldapConnection
+     *     LDAP connection to use to read the attributes of the user.
+     *
+     * @param username
+     *     The username of the user whose attributes are to be queried.
+     *
+     * @return
+     *     A map of parameter tokens generated from attributes on the user
+     *     currently bound under the given LDAP connection, as a map of token
+     *     name to corresponding value, or an empty map if no attributes are
+     *     specified or none are found on the user object.
+     *
+     * @throws GuacamoleException
+     *     If an error occurs retrieving the user DN or the attributes.
+     */
+    private Map<String, String> getAttributeTokens(LdapNetworkConnection ldapConnection,
+            Dn userDn) throws GuacamoleException {
+
+        // Get attributes from configuration information
+        List<String> attrList = confService.getAttributes();
+
+        // If there are no attributes there is no reason to search LDAP
+        if (attrList.isEmpty())
+            return Collections.<String, String>emptyMap();
+
+        // Build LDAP query parameters
+        String[] attrArray = attrList.toArray(new String[attrList.size()]);
+
+        Map<String, String> tokens = new HashMap<>();
         try {
 
-            // Retrieve group membership of the user that just authenticated
-            Set<String> effectiveGroups =
-                    userGroupService.getParentUserGroupIdentifiers(ldapConnection,
-                            ldapConnection.getAuthenticationDN());
+            // Get LDAP attributes by querying LDAP
+            Entry userEntry = ldapConnection.lookup(userDn, attrArray);
+            if (userEntry == null)
+                return Collections.<String, String>emptyMap();
 
-            // Return AuthenticatedUser if bind succeeds
-            AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
-            authenticatedUser.init(credentials, effectiveGroups);
-            return authenticatedUser;
+            Collection<Attribute> attributes = userEntry.getAttributes();
+            if (attributes == null)
+                return Collections.<String, String>emptyMap();
+
+            // Convert each retrieved attribute into a corresponding token
+            for (Attribute attr : attributes) {
+                tokens.put(TokenName.canonicalize(attr.getId(),
+                        LDAP_ATTRIBUTE_TOKEN_PREFIX), attr.getString());
+            }
 
         }
-
-        // Always disconnect
-        finally {
-            ldapService.disconnect(ldapConnection);
+        catch (LdapException e) {
+            throw new GuacamoleServerException("Could not query LDAP user attributes.", e);
         }
 
+        return tokens;
+
     }
 
     /**
@@ -263,28 +295,30 @@
      * @throws GuacamoleException
      *     If the UserContext cannot be created due to an error.
      */
-    public UserContext getUserContext(org.apache.guacamole.net.auth.AuthenticatedUser authenticatedUser)
+    public LDAPUserContext getUserContext(AuthenticatedUser authenticatedUser)
             throws GuacamoleException {
 
         // 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
-            UserContext 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/LDAPAuthenticationProvider.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProvider.java
index 31aa4e2..48e275c 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProvider.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPAuthenticationProvider.java
@@ -23,9 +23,11 @@
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser;
 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;
 
 /**
@@ -85,4 +87,19 @@
 
     }
 
+    @Override
+    public UserContext decorate(UserContext context,
+            AuthenticatedUser authenticatedUser, Credentials credentials)
+            throws GuacamoleException {
+
+        // Only decorate if the user authenticated against LDAP
+        if (!(authenticatedUser instanceof LDAPAuthenticatedUser))
+            return context;
+
+        // Apply LDAP-specific tokens to all connections / connection groups
+        return new TokenInjectingUserContext(context,
+                ((LDAPAuthenticatedUser) authenticatedUser).getTokens());
+
+    }
+
 }
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 94550a2..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,38 +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);
-                    }
+
                 }
 
             }
@@ -227,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);
         }
@@ -269,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);
     }
 
     /**
@@ -297,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;
             }
 
@@ -315,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 84%
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 6a4b8c0..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,10 +238,10 @@
      * @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 
+            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")
         );
     }
 
@@ -362,4 +338,39 @@
         );
     }
 
+    /**
+     * Returns names for custom LDAP user attributes.  By default no
+     * attributes will be returned.
+     *
+     * @return
+     *     Custom LDAP user attributes as configured in guacamole.properties.
+     *
+     * @throws GuacamoleException
+     *     If guacamole.properties cannot be parsed.
+     */
+    public List<String> getAttributes() throws GuacamoleException {
+        return environment.getProperty(
+            LDAPGuacamoleProperties.LDAP_USER_ATTRIBUTES,
+            Collections.<String>emptyList()
+        );
+    }
+    
+    /**
+     * Returns the name of the LDAP attribute used to enumerate
+     * members in a group, or "member" by default.
+     * 
+     * @return
+     *     The name of the LDAP attribute to use to enumerate
+     *     members in a group.
+     * 
+     * @throws GuacamoleException
+     *     If guacamole.properties connect be parsed.
+     */
+    public String getMemberAttribute() throws GuacamoleException {
+        return environment.getProperty(
+            LDAPGuacamoleProperties.LDAP_MEMBER_ATTRIBUTE,
+            "member"
+        );
+    }
+
 }
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 75%
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 340cbf5..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,11 +225,34 @@
     /**
      * 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"; }
 
     };
 
+    /**
+     * Custom attribute or attributes to query from Guacamole user's record in
+     * the LDAP directory.
+     */
+    public static final StringListProperty LDAP_USER_ATTRIBUTES =
+            new StringListProperty() {
+
+        @Override
+        public String getName() { return "ldap-user-attributes"; }
+
+    };
+    
+    /**
+     * LDAP attribute used to enumerate members of a group in the LDAP directory.
+     */
+    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 bae1da8..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,27 +20,32 @@
 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;
 import org.apache.guacamole.auth.ldap.group.UserGroupService;
+import org.apache.guacamole.auth.ldap.user.LDAPAuthenticatedUser;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
 import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.TokenInjectingConnection;
 import org.apache.guacamole.net.auth.simple.SimpleConnection;
 import org.apache.guacamole.protocol.GuacamoleConfiguration;
-import org.apache.guacamole.token.StandardTokens;
-import org.apache.guacamole.token.TokenFilter;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -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,50 +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);
-
-            // Build token filter containing credential tokens
-            TokenFilter tokenFilter = new TokenFilter();
-            StandardTokens.addStandardTokens(tokenFilter, user);
+            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('=');
@@ -180,19 +207,22 @@
 
                 }
 
-                // Filter the configuration, substituting all defined tokens
-                tokenFilter.filterValues(config.getParameters());
-
                 // 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
+                // authentication
+                if (user instanceof LDAPAuthenticatedUser)
+                    connection = new TokenInjectingConnection(connection,
+                            ((LDAPAuthenticatedUser) user).getTokens());
+
                 return connection;
 
             });
 
         }
-        catch (LDAPException e) {
+        catch (LdapException e) {
             throw new GuacamoleServerException("Error while querying for connections.", e);
         }
 
@@ -213,37 +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)(|(member=");
-        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 14cfd8e..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();
 
@@ -171,8 +182,8 @@
             ldapConnection,
             groupBaseDN,
             getGroupSearchFilter(),
-            Collections.singleton("member"),
-            userDN
+            Collections.singleton(confService.getMemberAttribute()),
+            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/AuthenticatedUser.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java
similarity index 60%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/AuthenticatedUser.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java
index 85f004b..4429643 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/AuthenticatedUser.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPAuthenticatedUser.java
@@ -20,7 +20,10 @@
 package org.apache.guacamole.auth.ldap.user;
 
 import com.google.inject.Inject;
+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;
@@ -29,7 +32,7 @@
  * An LDAP-specific implementation of AuthenticatedUser, associating a
  * particular set of credentials with the LDAP authentication provider.
  */
-public class AuthenticatedUser extends AbstractAuthenticatedUser {
+public class LDAPAuthenticatedUser extends AbstractAuthenticatedUser {
 
     /**
      * Reference to the authentication provider associated with this
@@ -44,27 +47,72 @@
     private Credentials credentials;
 
     /**
+     * Name/value pairs to be applied as parameter tokens when connections
+     * are established using this AuthenticatedUser.
+     */
+    private Map<String, String> tokens;
+
+    /**
      * The unique identifiers of all user groups which affect the permissions
      * 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 and set of
-     * effective user groups.
+     * Initializes this AuthenticatedUser with the given credentials,
+     * connection parameter tokens. and set of effective user groups.
      *
      * @param credentials
      *     The credentials provided when this user was authenticated.
      *
+     * @param tokens
+     *     A Map of all name/value pairs that should be applied as parameter
+     *     tokens when connections are established using the AuthenticatedUser.
+     *
      * @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, 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
+     * AuthenticatedUser.
+     *
+     * @return
+     *     A Map of all name/value pairs that should be applied as parameter
+     *     tokens when connections are established using this
+     *     AuthenticatedUser.
+     */
+    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/UserContext.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java
similarity index 94%
rename from extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserContext.java
rename to extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/LDAPUserContext.java
index 826b4ec..b5c789e 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserContext.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,19 +39,12 @@
 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
  * connections and users from the LDAP directory.
  */
-public class UserContext extends AbstractUserContext {
-
-    /**
-     * Logger for this class.
-     */
-    private final Logger logger = LoggerFactory.getLogger(UserContext.class);
+public class LDAPUserContext extends AbstractUserContext {
 
     /**
      * Service for retrieving Guacamole connections from the LDAP server.
@@ -109,7 +102,7 @@
 
     /**
      * Initializes this UserContext using the provided AuthenticatedUser and
-     * LDAPConnection.
+     * LdapNetworkConnection.
      *
      * @param user
      *     The AuthenticatedUser representing the user that authenticated. This
@@ -124,7 +117,7 @@
      *     If associated data stored within the LDAP directory cannot be
      *     queried due to an error.
      */
-    public void init(AuthenticatedUser user, LDAPConnection ldapConnection)
+    public void init(AuthenticatedUser user, LdapNetworkConnection ldapConnection)
             throws GuacamoleException {
 
         // Query all accessible users
diff --git a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java
index 3f12ae8..ba29983 100644
--- a/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/user/UserService.java
@@ -20,16 +20,20 @@
 package org.apache.guacamole.auth.ldap.user;
 
 import com.google.inject.Inject;
-import com.novell.ldap.LDAPConnection;
-import com.novell.ldap.LDAPEntry;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.List;
 import java.util.Map;
-import org.apache.guacamole.auth.ldap.ConfigurationService;
-import org.apache.guacamole.auth.ldap.EscapingService;
+import org.apache.directory.api.ldap.model.entry.Entry;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidDnException;
+import org.apache.directory.api.ldap.model.exception.LdapInvalidAttributeValueException;
+import org.apache.directory.api.ldap.model.name.Dn;
+import org.apache.directory.api.ldap.model.name.Rdn;
+import org.apache.directory.ldap.client.api.LdapNetworkConnection;
+import org.apache.guacamole.auth.ldap.conf.ConfigurationService;
 import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.auth.ldap.LDAPGuacamoleProperties;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.auth.ldap.conf.LDAPGuacamoleProperties;
 import org.apache.guacamole.auth.ldap.ObjectQueryService;
 import org.apache.guacamole.net.auth.User;
 import org.apache.guacamole.net.auth.simple.SimpleUser;
@@ -45,13 +49,7 @@
     /**
      * Logger for this class.
      */
-    private final Logger logger = LoggerFactory.getLogger(UserService.class);
-
-    /**
-     * Service for escaping parts of LDAP queries.
-     */
-    @Inject
-    private EscapingService escapingService;
+    private static final Logger logger = LoggerFactory.getLogger(UserService.class);
 
     /**
      * Service for retrieving LDAP server configuration information.
@@ -81,12 +79,12 @@
      * @throws GuacamoleException
      *     If an error occurs preventing retrieval of users.
      */
-    public Map<String, User> getUsers(LDAPConnection ldapConnection)
+    public Map<String, User> getUsers(LdapNetworkConnection ldapConnection)
             throws GuacamoleException {
 
         // Retrieve all visible user objects
         Collection<String> attributes = confService.getUsernameAttributes();
-        List<LDAPEntry> results = queryService.search(ldapConnection,
+        List<Entry> results = queryService.search(ldapConnection,
                 confService.getUserBaseDN(),
                 confService.getUserSearchFilter(),
                 attributes,
@@ -96,15 +94,21 @@
         return queryService.asMap(results, entry -> {
 
             // Get username from record
-            String username = queryService.getIdentifier(entry, attributes);
-            if (username == null) {
-                logger.warn("User \"{}\" is missing a username attribute "
-                        + "and will be ignored.", entry.getDN());
+            try {
+                String username = queryService.getIdentifier(entry, attributes);
+                if (username == null) {
+                    logger.warn("User \"{}\" is missing a username attribute "
+                            + "and will be ignored.", entry.getDn().toString());
+                    return null;
+                }
+                
+                return new SimpleUser(username);
+            }
+            catch (LdapInvalidAttributeValueException e) {
+                
                 return null;
             }
 
-            return new SimpleUser(username);
-
         });
 
     }
@@ -130,19 +134,19 @@
      *     If an error occurs while querying the user DNs, or if the username
      *     attribute property cannot be parsed within guacamole.properties.
      */
-    public List<String> getUserDNs(LDAPConnection ldapConnection,
+    public List<Dn> getUserDNs(LdapNetworkConnection ldapConnection,
             String username) throws GuacamoleException {
 
         // Retrieve user objects having a matching username
-        List<LDAPEntry> results = queryService.search(ldapConnection,
+        List<Entry> results = queryService.search(ldapConnection,
                 confService.getUserBaseDN(),
                 confService.getUserSearchFilter(),
                 confService.getUsernameAttributes(),
                 username);
 
         // Build list of all DNs for retrieved users
-        List<String> userDNs = new ArrayList<>(results.size());
-        results.forEach(entry -> userDNs.add(entry.getDN()));
+        List<Dn> userDNs = new ArrayList<>(results.size());
+        results.forEach(entry -> userDNs.add(entry.getDn()));
 
         return userDNs;
 
@@ -164,7 +168,7 @@
      *     If required properties are missing, and thus the user DN cannot be
      *     determined.
      */
-    public String deriveUserDN(String username)
+    public Dn deriveUserDN(String username)
             throws GuacamoleException {
 
         // Pull username attributes from properties
@@ -181,10 +185,13 @@
         }
 
         // Derive user DN from base DN
-        return
-                    escapingService.escapeDN(usernameAttributes.get(0))
-            + "=" + escapingService.escapeDN(username)
-            + "," + confService.getUserBaseDN();
+        try {
+            return new Dn(new Rdn(usernameAttributes.get(0), username),
+                confService.getUserBaseDN());
+        }
+        catch (LdapInvalidAttributeValueException | LdapInvalidDnException e) {
+            throw new GuacamoleServerException("Error trying to derive user DN.", e);
+        }
 
     }
 
diff --git a/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json
index 2d4d090..7002388 100644
--- a/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-ldap/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "LDAP Authentication",
     "namespace" : "guac-ldap",
diff --git a/extensions/guacamole-auth-openid/pom.xml b/extensions/guacamole-auth-openid/pom.xml
index 63ecd51..25158c8 100644
--- a/extensions/guacamole-auth-openid/pom.xml
+++ b/extensions/guacamole-auth-openid/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-openid</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-openid</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -210,7 +210,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
@@ -246,6 +246,14 @@
             <version>2.5</version>
             <scope>provided</scope>
         </dependency>
+        
+        <!-- Jersey - JAX-RS Implementation -->
+        <dependency>
+            <groupId>javax.ws.rs</groupId>
+            <artifactId>jsr311-api</artifactId>
+            <version>1.1.1</version>
+            <scope>provided</scope>
+        </dependency>
 
     </dependencies>
 
diff --git a/extensions/guacamole-auth-openid/src/licenses/NOTICE b/extensions/guacamole-auth-openid/src/licenses/NOTICE
index 97e6130..39de3ec 100644
--- a/extensions/guacamole-auth-openid/src/licenses/NOTICE
+++ b/extensions/guacamole-auth-openid/src/licenses/NOTICE
@@ -1,5 +1,5 @@
 Apache Guacamole
-Copyright 2018 The Apache Software Foundation
+Copyright 2019 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java
index c742d89..9d889a8 100644
--- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java
+++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/conf/ConfigurationService.java
@@ -20,10 +20,12 @@
 package org.apache.guacamole.auth.openid.conf;
 
 import com.google.inject.Inject;
+import java.net.URI;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.environment.Environment;
 import org.apache.guacamole.properties.IntegerGuacamoleProperty;
 import org.apache.guacamole.properties.StringGuacamoleProperty;
+import org.apache.guacamole.properties.URIGuacamoleProperty;
 
 /**
  * Service for retrieving configuration information regarding the OpenID
@@ -63,8 +65,8 @@
     /**
      * The authorization endpoint (URI) of the OpenID service.
      */
-    private static final StringGuacamoleProperty OPENID_AUTHORIZATION_ENDPOINT =
-            new StringGuacamoleProperty() {
+    private static final URIGuacamoleProperty OPENID_AUTHORIZATION_ENDPOINT =
+            new URIGuacamoleProperty() {
 
         @Override
         public String getName() { return "openid-authorization-endpoint"; }
@@ -75,8 +77,8 @@
      * The endpoint (URI) of the JWKS service which defines how received ID
      * tokens (JWTs) shall be validated.
      */
-    private static final StringGuacamoleProperty OPENID_JWKS_ENDPOINT =
-            new StringGuacamoleProperty() {
+    private static final URIGuacamoleProperty OPENID_JWKS_ENDPOINT =
+            new URIGuacamoleProperty() {
 
         @Override
         public String getName() { return "openid-jwks-endpoint"; }
@@ -174,8 +176,8 @@
      * authentication process is complete. This must be the full URL that a
      * user would enter into their browser to access Guacamole.
      */
-    private static final StringGuacamoleProperty OPENID_REDIRECT_URI =
-            new StringGuacamoleProperty() {
+    private static final URIGuacamoleProperty OPENID_REDIRECT_URI =
+            new URIGuacamoleProperty() {
 
         @Override
         public String getName() { return "openid-redirect-uri"; }
@@ -200,7 +202,7 @@
      *     If guacamole.properties cannot be parsed, or if the authorization
      *     endpoint property is missing.
      */
-    public String getAuthorizationEndpoint() throws GuacamoleException {
+    public URI getAuthorizationEndpoint() throws GuacamoleException {
         return environment.getRequiredProperty(OPENID_AUTHORIZATION_ENDPOINT);
     }
 
@@ -236,7 +238,7 @@
      *     If guacamole.properties cannot be parsed, or if the redirect URI
      *     property is missing.
      */
-    public String getRedirectURI() throws GuacamoleException {
+    public URI getRedirectURI() throws GuacamoleException {
         return environment.getRequiredProperty(OPENID_REDIRECT_URI);
     }
 
@@ -270,7 +272,7 @@
      *     If guacamole.properties cannot be parsed, or if the JWKS endpoint
      *     property is missing.
      */
-    public String getJWKSEndpoint() throws GuacamoleException {
+    public URI getJWKSEndpoint() throws GuacamoleException {
         return environment.getRequiredProperty(OPENID_JWKS_ENDPOINT);
     }
 
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 d99c367..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
@@ -19,8 +19,8 @@
 
 package org.apache.guacamole.auth.openid.form;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
+import java.net.URI;
+import javax.ws.rs.core.UriBuilder;
 import org.apache.guacamole.form.Field;
 
 /**
@@ -38,7 +38,7 @@
     /**
      * The full URI which the field should link to.
      */
-    private final String authorizationURI;
+    private final URI authorizationURI;
 
     /**
      * Creates a new field which requests authentication via OpenID connect.
@@ -69,26 +69,19 @@
      *     A random string unique to this request. To defend against replay
      *     attacks, this value must cease being valid after its first use.
      */
-    public TokenField(String authorizationEndpoint, String scope,
-            String clientID, String redirectURI, String nonce) {
+    public TokenField(URI authorizationEndpoint, String scope,
+            String clientID, URI redirectURI, String nonce) {
 
         // Init base field properties
         super(PARAMETER_NAME, "GUAC_OPENID_TOKEN");
 
-        // Build authorization URI from given values
-        try {
-            this.authorizationURI = authorizationEndpoint
-                    + "?scope=" + URLEncoder.encode(scope, "UTF-8")
-                    + "&response_type=id_token"
-                    + "&client_id=" + URLEncoder.encode(clientID, "UTF-8")
-                    + "&redirect_uri=" + URLEncoder.encode(redirectURI, "UTF-8")
-                    + "&nonce=" + nonce;
-        }
-
-        // Java is required to provide UTF-8 support
-        catch (UnsupportedEncodingException e) {
-            throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
-        }
+        this.authorizationURI = UriBuilder.fromUri(authorizationEndpoint)
+                .queryParam("scope", scope)
+                .queryParam("response_type", "id_token")
+                .queryParam("client_id", clientID)
+                .queryParam("redirect_uri", redirectURI)
+                .queryParam("nonce", nonce)
+                .build();
 
     }
 
@@ -100,7 +93,7 @@
      *     The full URI that this field should link to.
      */
     public String getAuthorizationURI() {
-        return authorizationURI;
+        return authorizationURI.toString();
     }
 
 }
diff --git a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java
index cde4f89..5efb09d 100644
--- a/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java
+++ b/extensions/guacamole-auth-openid/src/main/java/org/apache/guacamole/auth/openid/token/TokenValidationService.java
@@ -74,7 +74,7 @@
     public String processUsername(String token) throws GuacamoleException {
 
         // Validating the token requires a JWKS key resolver
-        HttpsJwks jwks = new HttpsJwks(confService.getJWKSEndpoint());
+        HttpsJwks jwks = new HttpsJwks(confService.getJWKSEndpoint().toString());
         HttpsJwksVerificationKeyResolver resolver = new HttpsJwksVerificationKeyResolver(jwks);
 
         // Create JWT consumer for validating received token
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 91d33f1..b15f83f 100644
--- a/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-openid/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "OpenID Authentication Extension",
     "namespace" : "guac-openid",
@@ -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/pom.xml b/extensions/guacamole-auth-quickconnect/pom.xml
index 8acf5f6..df290b8 100644
--- a/extensions/guacamole-auth-quickconnect/pom.xml
+++ b/extensions/guacamole-auth-quickconnect/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-quickconnect</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-quickconnect</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -188,7 +188,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
diff --git a/extensions/guacamole-auth-quickconnect/src/licenses/NOTICE b/extensions/guacamole-auth-quickconnect/src/licenses/NOTICE
index 97e6130..39de3ec 100644
--- a/extensions/guacamole-auth-quickconnect/src/licenses/NOTICE
+++ b/extensions/guacamole-auth-quickconnect/src/licenses/NOTICE
@@ -1,5 +1,5 @@
 Apache Guacamole
-Copyright 2018 The Apache Software Foundation
+Copyright 2019 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/extensions/guacamole-auth-quickconnect/src/main/java/org/apache/guacamole/auth/quickconnect/QuickConnectDirectory.java b/extensions/guacamole-auth-quickconnect/src/main/java/org/apache/guacamole/auth/quickconnect/QuickConnectDirectory.java
index 37b07ba..cec0432 100644
--- a/extensions/guacamole-auth-quickconnect/src/main/java/org/apache/guacamole/auth/quickconnect/QuickConnectDirectory.java
+++ b/extensions/guacamole-auth-quickconnect/src/main/java/org/apache/guacamole/auth/quickconnect/QuickConnectDirectory.java
@@ -107,7 +107,7 @@
         String name = QCParser.getName(config);
 
         // Create a new connection and set the parent identifier.
-        Connection connection = new SimpleConnection(name, newConnectionId, config);
+        Connection connection = new SimpleConnection(name, newConnectionId, config, true);
         connection.setParentIdentifier(QuickConnectUserContext.ROOT_IDENTIFIER);
 
         // Place the object in this directory.
diff --git a/extensions/guacamole-auth-quickconnect/src/main/java/org/apache/guacamole/auth/quickconnect/QuickConnectionGroup.java b/extensions/guacamole-auth-quickconnect/src/main/java/org/apache/guacamole/auth/quickconnect/QuickConnectionGroup.java
index cbce379..dbb6934 100644
--- a/extensions/guacamole-auth-quickconnect/src/main/java/org/apache/guacamole/auth/quickconnect/QuickConnectionGroup.java
+++ b/extensions/guacamole-auth-quickconnect/src/main/java/org/apache/guacamole/auth/quickconnect/QuickConnectionGroup.java
@@ -108,8 +108,8 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info) 
-            throws GuacamoleException {
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
         // This group does not support connections
         throw new GuacamoleSecurityException("Permission denied.");
     }
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 7f1e4da..48eeb42 100644
--- a/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-quickconnect/src/main/resources/guac-manifest.json
@@ -1,5 +1,5 @@
 {
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"             : "Adhoc Guacamole Connections",
     "namespace"        : "quickconnect",
@@ -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/pom.xml b/extensions/guacamole-auth-radius/pom.xml
index d07a60a..abdb2d3 100644
--- a/extensions/guacamole-auth-radius/pom.xml
+++ b/extensions/guacamole-auth-radius/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-radius</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-radius</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -178,7 +178,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
 
@@ -186,9 +186,16 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
+        
+        <!-- Guava - Utility Library -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>27.0.1-jre</version>
+        </dependency>
 
         <!-- Guice -->
         <dependency>
diff --git a/extensions/guacamole-auth-radius/src/licenses/LICENSE b/extensions/guacamole-auth-radius/src/licenses/LICENSE
index 7ad231d..cb58a78 100644
--- a/extensions/guacamole-auth-radius/src/licenses/LICENSE
+++ b/extensions/guacamole-auth-radius/src/licenses/LICENSE
@@ -238,6 +238,15 @@
         Apache v2.0 (bundled/guice-3.0/COPYING)
 
 
+Guava: Google Core Libraries for Java (https://github.com/google/guava)
+-----------------------------------------------------------------------
+
+    Version: 27.0.1-jre
+    From: 'Google Inc.' (http://www.google.com/)
+    License(s):
+        Apache v2.0 (bundled/guava-27.0.1-jre/COPYING)
+
+
 JRadius (https://github.com/coova/jradius)
 ------------------------------------------
 
diff --git a/extensions/guacamole-auth-radius/src/licenses/NOTICE b/extensions/guacamole-auth-radius/src/licenses/NOTICE
index 97e6130..39de3ec 100644
--- a/extensions/guacamole-auth-radius/src/licenses/NOTICE
+++ b/extensions/guacamole-auth-radius/src/licenses/NOTICE
@@ -1,5 +1,5 @@
 Apache Guacamole
-Copyright 2018 The Apache Software Foundation
+Copyright 2019 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/extensions/guacamole-auth-totp/src/licenses/bundled/guava-18.0/COPYING b/extensions/guacamole-auth-radius/src/licenses/bundled/guava-27.0.1-jre/COPYING
similarity index 100%
copy from extensions/guacamole-auth-totp/src/licenses/bundled/guava-18.0/COPYING
copy to extensions/guacamole-auth-radius/src/licenses/bundled/guava-27.0.1-jre/COPYING
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 852eb72..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
@@ -19,15 +19,13 @@
 
 package org.apache.guacamole.auth.radius;
 
+import com.google.common.io.BaseEncoding;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import java.lang.IllegalArgumentException;
-import java.nio.charset.Charset;
 import java.util.Arrays;
 import javax.servlet.http.HttpServletRequest;
-import javax.xml.bind.DatatypeConverter;
 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;
@@ -44,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
@@ -55,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.
@@ -69,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);
@@ -99,13 +109,16 @@
         }
 
         // We have the required attributes - convert to strings and then generate the additional login box/field
-        String replyMsg = replyAttr.toString();
-        String radiusState = DatatypeConverter.printHexBinary(stateAttr.getValue().getBytes());
-        Field radiusResponseField = new RadiusChallengeResponseField(replyMsg);
+        String replyMsg = replyAttr.getValue().toString();
+        String radiusState = BaseEncoding.base16().encode(stateAttr.getValue().getBytes());
+        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)));
     }
 
     /**
@@ -136,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;
@@ -164,7 +177,7 @@
                     throw new GuacamoleInvalidCredentialsException("Authentication error.", CredentialsInfo.USERNAME_PASSWORD);
                 }
 
-                byte[] stateBytes = DatatypeConverter.parseHexBinary(stateString);
+                byte[] stateBytes = BaseEncoding.base16().decode(stateString);
                 radPack = radiusService.sendChallengeResponse(credentials.getUsername(),
                                                               challengeResponse,
                                                               stateBytes);
@@ -202,12 +215,14 @@
 
         // Received AccessChallenge packet, more credentials required to complete authentication
         else if (radPack instanceof AccessChallenge) {
-            CredentialsInfo expectedCredentials = getRadiusChallenge(radPack);
+            GuacamoleRadiusChallenge challenge = getRadiusChallenge(radPack);
 
-            if (expectedCredentials == null)
+            if (challenge == null)
                 throw new GuacamoleInvalidCredentialsException("Authentication error.", CredentialsInfo.USERNAME_PASSWORD);
 
-            throw new GuacamoleInsufficientCredentialsException("LOGIN.INFO_RADIUS_ADDL_REQUIRED", expectedCredentials);
+            throw new GuacamoleInsufficientCredentialsException(
+                    challenge.getChallengeText(),
+                    challenge.getExpectedCredentials());
         }
 
         // Something unanticipated happened, so panic and go back to login.
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java
index 37ecb79..4224f77 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusAuthenticationProviderModule.java
@@ -20,10 +20,17 @@
 package org.apache.guacamole.auth.radius;
 
 import com.google.inject.AbstractModule;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.Security;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.auth.radius.conf.ConfigurationService;
+import org.apache.guacamole.auth.radius.conf.RadiusAuthenticationProtocol;
+import org.apache.guacamole.auth.radius.conf.RadiusGuacamoleProperties;
 import org.apache.guacamole.environment.Environment;
 import org.apache.guacamole.environment.LocalEnvironment;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
 
 /**
  * Guice module which configures RADIUS-specific injections.
@@ -57,6 +64,23 @@
 
         // Get local environment
         this.environment = new LocalEnvironment();
+        
+        // Check for MD4 requirement
+        RadiusAuthenticationProtocol authProtocol = environment.getProperty(RadiusGuacamoleProperties.RADIUS_AUTH_PROTOCOL);
+        RadiusAuthenticationProtocol innerProtocol = environment.getProperty(RadiusGuacamoleProperties.RADIUS_EAP_TTLS_INNER_PROTOCOL);
+        if (authProtocol == RadiusAuthenticationProtocol.MSCHAPv1 
+                    || authProtocol == RadiusAuthenticationProtocol.MSCHAPv2
+                    || innerProtocol == RadiusAuthenticationProtocol.MSCHAPv1 
+                    || innerProtocol == RadiusAuthenticationProtocol.MSCHAPv2) {
+            
+            try {
+                MessageDigest.getInstance("MD4");
+            }
+            catch (NoSuchAlgorithmException e) {
+                Security.addProvider(new BouncyCastleProvider());
+            }
+            
+        }
 
         // Store associated auth provider
         this.authProvider = authProvider;
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java
index ec82a63..c8a21d6 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusConnectionService.java
@@ -27,6 +27,8 @@
 import java.security.NoSuchAlgorithmException;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.auth.radius.conf.ConfigurationService;
+import org.apache.guacamole.auth.radius.conf.RadiusAuthenticationProtocol;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import net.jradius.client.RadiusClient;
@@ -62,8 +64,7 @@
      */
     @Inject
     private ConfigurationService confService;
-
-
+    
     /**
      * Creates a new instance of RadiusClient, configured with parameters
      * from guacamole.properties.
@@ -115,8 +116,8 @@
      *     not configured when the client is set up for a tunneled
      *     RADIUS connection.
      */
-    private RadiusAuthenticator setupRadiusAuthenticator(RadiusClient radiusClient)
-            throws GuacamoleException {
+    private RadiusAuthenticator setupRadiusAuthenticator(
+            RadiusClient radiusClient) throws GuacamoleException {
 
         // If we don't have a radiusClient object, yet, don't go any further.
         if (radiusClient == null) {
@@ -125,7 +126,9 @@
             return null;
         }
 
-        RadiusAuthenticator radAuth = radiusClient.getAuthProtocol(confService.getRadiusAuthProtocol());
+        RadiusAuthenticator radAuth = radiusClient.getAuthProtocol(
+                confService.getRadiusAuthProtocol().toString());
+        
         if (radAuth == null)
             throw new GuacamoleException("Could not get a valid RadiusAuthenticator for specified protocol: " + confService.getRadiusAuthProtocol());
 
@@ -157,11 +160,13 @@
 
         // If we're using EAP-TTLS, we need to define tunneled protocol
         if (radAuth instanceof EAPTTLSAuthenticator) {
-            String innerProtocol = confService.getRadiusEAPTTLSInnerProtocol();
+            RadiusAuthenticationProtocol innerProtocol =
+                    confService.getRadiusEAPTTLSInnerProtocol();
+            
             if (innerProtocol == null)
-                throw new GuacamoleException("Trying to use EAP-TTLS, but no inner protocol specified.");
+                throw new GuacamoleException("Missing or invalid inner protocol for EAP-TTLS.");
 
-            ((EAPTTLSAuthenticator)radAuth).setInnerProtocol(innerProtocol);
+            ((EAPTTLSAuthenticator)radAuth).setInnerProtocol(innerProtocol.toString());
         }
 
         return radAuth;
@@ -236,14 +241,21 @@
 
             radAuth.setupRequest(radiusClient, radAcc);
             radAuth.processRequest(radAcc);
-            RadiusResponse reply = radiusClient.sendReceive(radAcc, confService.getRadiusMaxRetries());
+            RadiusResponse reply = radiusClient.sendReceive(radAcc,
+                    confService.getRadiusMaxRetries());
 
             // We receive a Challenge not asking for user input, so silently process the challenge
-            while((reply instanceof AccessChallenge) && (reply.findAttribute(Attr_ReplyMessage.TYPE) == null)) {
+            while((reply instanceof AccessChallenge) 
+                    && (reply.findAttribute(Attr_ReplyMessage.TYPE) == null)) {
+                
                 radAuth.processChallenge(radAcc, reply);
-                reply = radiusClient.sendReceive(radAcc, confService.getRadiusMaxRetries());
+                reply = radiusClient.sendReceive(radAcc,
+                        confService.getRadiusMaxRetries());
+                
             }
+            
             return reply;
+            
         }
         catch (RadiusException e) {
             logger.error("Unable to complete authentication.", e.getMessage());
@@ -282,8 +294,8 @@
      * @throws GuacamoleException
      *     If an error is encountered trying to talk to the RADIUS server.
      */
-    public RadiusPacket sendChallengeResponse(String username, String response, byte[] state)
-            throws GuacamoleException {
+    public RadiusPacket sendChallengeResponse(String username, String response,
+            byte[] state) throws GuacamoleException {
 
         if (username == null || username.isEmpty()) {
             logger.error("Challenge/response to RADIUS requires a username.");
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/ConfigurationService.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/ConfigurationService.java
similarity index 92%
rename from extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/ConfigurationService.java
rename to extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/ConfigurationService.java
index 381ea13..2809f7c 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/ConfigurationService.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/ConfigurationService.java
@@ -17,11 +17,12 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.radius;
+package org.apache.guacamole.auth.radius.conf;
 
 import com.google.inject.Inject;
 import java.io.File;
 import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
 import org.apache.guacamole.environment.Environment;
 
 /**
@@ -123,8 +124,9 @@
      * @throws GuacamoleException
      *     If guacamole.properties cannot be parsed.
      */
-    public String getRadiusAuthProtocol() throws GuacamoleException {
-        return environment.getProperty(
+    public RadiusAuthenticationProtocol getRadiusAuthProtocol()
+            throws GuacamoleException {
+        return environment.getRequiredProperty(
             RadiusGuacamoleProperties.RADIUS_AUTH_PROTOCOL
         );
     }
@@ -309,12 +311,21 @@
      *     an EAP-TTLS RADIUS connection. 
      *     
      * @throws GuacamoleException
-     *     If guacamole.properties cannot be parsed.
+     *     If guacamole.properties cannot be parsed, or if EAP-TTLS is specified
+     *     as the inner protocol.
      */
-    public String getRadiusEAPTTLSInnerProtocol() throws GuacamoleException {
-        return environment.getProperty(
+    public RadiusAuthenticationProtocol getRadiusEAPTTLSInnerProtocol()
+            throws GuacamoleException {
+        
+        RadiusAuthenticationProtocol authProtocol = environment.getProperty(
             RadiusGuacamoleProperties.RADIUS_EAP_TTLS_INNER_PROTOCOL
         );
+        
+        if (authProtocol == RadiusAuthenticationProtocol.EAP_TTLS)
+            throw new GuacamoleServerException("Invalid inner protocol specified for EAP-TTLS.");
+        
+        return authProtocol;
+        
     }
 
 }
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocol.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocol.java
new file mode 100644
index 0000000..e64a695
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocol.java
@@ -0,0 +1,118 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.radius.conf;
+
+/**
+ * This enum represents supported RADIUS authentication protocols for
+ * the guacamole-auth-radius extension.
+ */
+public enum RadiusAuthenticationProtocol {
+    
+    /**
+     * Password Authentication Protocol (PAP)
+     */
+    PAP("pap"),
+    
+    /**
+     * Challenge-Handshake Authentication Protocol (CHAP)
+     */
+    CHAP("chap"),
+    
+    /**
+     * Microsoft implementation of CHAP, Version 1 (MS-CHAPv1)
+     */
+    MSCHAPv1("mschapv1"),
+    
+    /**
+     * Microsoft implementation of CHAP, Version 2 (MS-CHAPv2)
+     */
+    MSCHAPv2("mschapv2"),
+    
+    /**
+     * Extensible Authentication Protocol (EAP) with MD5 Hashing (EAP-MD5)
+     */
+    EAP_MD5("eap-md5"),
+
+    /**
+     * Extensible Authentication Protocol (EAP) with TLS encryption (EAP-TLS).
+     */
+    EAP_TLS("eap-tls"),
+
+    /**
+     * Extensible Authentication Protocol (EAP) with Tunneled TLS (EAP-TTLS).
+     */
+    EAP_TTLS("eap-ttls");
+
+    /**
+     * This variable stores the string value of the protocol, and is also
+     * used within the extension to pass to JRadius for configuring the
+     * library to talk to the RADIUS server.
+     */
+    private final String strValue;
+    
+    /**
+     * Create a new RadiusAuthenticationProtocol object having the
+     * given string value.
+     * 
+     * @param strValue
+     *     The value of the protocol to store as a string, which will be used
+     *     in specifying the protocol within the guacamole.properties file, and
+     *     will also be used by the JRadius library for its configuration.
+     */
+    RadiusAuthenticationProtocol(String strValue) {
+        this.strValue = strValue;
+    }
+    
+    /**
+    * {@inheritDoc}
+    * <p>
+    * This function returns the stored string values of the selected RADIUS
+    * protocol, which is used both in Guacamole configuration and also to pass
+    * on to the JRadius library for its configuration.
+    * 
+    * @return
+    *     The string value stored for the selected RADIUS protocol.
+    */
+    @Override
+    public String toString() {
+        return strValue;
+    }
+    
+    /**
+     * For a given String value, return the enum value that matches that string,
+     * or null if no matchi is found.
+     * 
+     * @param value
+     *     The string value to search for in the list of enums.
+     * 
+     * @return
+     *     The RadiusAuthenticationProtocol value that is identified by the
+     *     provided String value.
+     */
+    public static RadiusAuthenticationProtocol getEnum(String value) {
+    
+        for (RadiusAuthenticationProtocol v : values())
+            if(v.toString().equals(value))
+                return v;
+        
+        return null;
+    }
+    
+}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocolProperty.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocolProperty.java
new file mode 100644
index 0000000..c92c0a3
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusAuthenticationProtocolProperty.java
@@ -0,0 +1,54 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.radius.conf;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+import org.apache.guacamole.properties.GuacamoleProperty;
+
+/**
+ * A GuacamoleProperty whose value is a RadiusAuthenticationProtocol.
+ */
+public abstract class RadiusAuthenticationProtocolProperty
+        implements GuacamoleProperty<RadiusAuthenticationProtocol> {
+    
+    @Override
+    public RadiusAuthenticationProtocol parseValue(String value)
+            throws GuacamoleException {
+        
+        // Nothing provided, nothing returned
+        if (value == null)
+            return null;
+        
+        // Attempt to parse the string value
+        RadiusAuthenticationProtocol authProtocol = 
+                RadiusAuthenticationProtocol.getEnum(value);
+        
+        // Throw an exception if nothing matched.
+        if (authProtocol == null)
+            throw new GuacamoleServerException(
+                    "Invalid or unsupported RADIUS authentication protocol.");
+        
+        // Return the answer
+        return authProtocol;
+        
+    }
+    
+}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusGuacamoleProperties.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusGuacamoleProperties.java
similarity index 93%
rename from extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusGuacamoleProperties.java
rename to extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusGuacamoleProperties.java
index aaa445e..af6839b 100644
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/RadiusGuacamoleProperties.java
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/conf/RadiusGuacamoleProperties.java
@@ -17,7 +17,7 @@
  * under the License.
  */
 
-package org.apache.guacamole.auth.radius;
+package org.apache.guacamole.auth.radius.conf;
 
 import org.apache.guacamole.properties.BooleanGuacamoleProperty;
 import org.apache.guacamole.properties.FileGuacamoleProperty;
@@ -81,7 +81,8 @@
     /**
      * The authentication protocol of the RADIUS server to connect to when authenticating users.
      */
-    public static final StringGuacamoleProperty RADIUS_AUTH_PROTOCOL = new StringGuacamoleProperty() {
+    public static final RadiusAuthenticationProtocolProperty RADIUS_AUTH_PROTOCOL =
+            new RadiusAuthenticationProtocolProperty() {
 
         @Override
         public String getName() { return "radius-auth-protocol"; }
@@ -181,7 +182,8 @@
     /**
      * The tunneled protocol to use inside a RADIUS EAP-TTLS connection.
      */
-    public static final StringGuacamoleProperty RADIUS_EAP_TTLS_INNER_PROTOCOL = new StringGuacamoleProperty() {
+    public static final RadiusAuthenticationProtocolProperty RADIUS_EAP_TTLS_INNER_PROTOCOL =
+            new RadiusAuthenticationProtocolProperty() {
 
         @Override
         public String getName() { return "radius-eap-ttls-inner-protocol"; }
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java
new file mode 100644
index 0000000..4589794
--- /dev/null
+++ b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/GuacamoleRadiusChallenge.java
@@ -0,0 +1,77 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.auth.radius.form;
+
+import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
+
+/**
+ * Stores the RADIUS challenge message and expected credentials in a single
+ * object.
+ */
+public class GuacamoleRadiusChallenge {
+    
+    /**
+     * The challenge text sent by the RADIUS server.
+     */
+    private final String challengeText;
+    
+    /**
+     * The expected credentials that need to be provided to satisfy the
+     * RADIUS authentication challenge.
+     */
+    private final CredentialsInfo expectedCredentials;
+    
+    /**
+     * Creates a new GuacamoleRadiusChallenge object with the provided
+     * challenge message and expected credentials.
+     * 
+     * @param challengeText
+     *     The challenge message sent by the RADIUS server.
+     * 
+     * @param expectedCredentials 
+     *     The credentials required to complete the challenge.
+     */
+    public GuacamoleRadiusChallenge(String challengeText,
+            CredentialsInfo expectedCredentials) {
+        this.challengeText = challengeText;
+        this.expectedCredentials = expectedCredentials;
+    }
+    
+    /**
+     * Returns the challenge message provided by the RADIUS server.
+     * 
+     * @return
+     *     The challenge message provided by the RADIUS server.
+     */
+    public String getChallengeText() {
+        return challengeText;
+    }
+    
+    /**
+     * Returns the credentials required to satisfy the RADIUS challenge.
+     * 
+     * @return 
+     *     The credentials required to satisfy the RADIUS challenge.
+     */
+    public CredentialsInfo getExpectedCredentials() {
+        return expectedCredentials;
+    }
+    
+}
diff --git a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java b/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java
deleted file mode 100644
index 32ceb90..0000000
--- a/extensions/guacamole-auth-radius/src/main/java/org/apache/guacamole/auth/radius/form/RadiusChallengeResponseField.java
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-package org.apache.guacamole.auth.radius.form;
-
-import org.apache.guacamole.form.Field;
-
-/**
- * A form used to prompt the user for additional information when
- * the RADIUS server sends a challenge back to the user with a reply
- * message.
- */
-public class RadiusChallengeResponseField extends Field {
-
-    /**
-     * The field returned by the RADIUS challenge/response.
-     */
-    public static final String PARAMETER_NAME = "guac-radius-challenge-response";
-
-    /**
-     * The type of field to initialize for the challenge/response.
-     */
-    private static final String RADIUS_FIELD_TYPE = "GUAC_RADIUS_CHALLENGE_RESPONSE";
-
-    /**
-     * The message the RADIUS server sent back in the challenge.
-     */
-    private final String challenge;
-
-    /**
-     * Initialize the field with the challenge sent back by the RADIUS server.
-     *
-     * @param challenge
-     *     The challenge message sent back by the RADIUS server.
-     */
-    public RadiusChallengeResponseField(String challenge) {
-        super(PARAMETER_NAME, RADIUS_FIELD_TYPE);
-        this.challenge = challenge;
-
-    }
-
-    /**
-     * Get the challenge sent by the RADIUS server.
-     *
-     * @return
-     *     A String that indicates the challenge returned
-     *     by the RADIUS server.
-     */
-    public String getChallenge() {
-        return challenge;
-    }
-}
diff --git a/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js b/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
index dab0ffc..a3d72bf 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
+++ b/extensions/guacamole-auth-radius/src/main/resources/config/radiusConfig.js
@@ -23,13 +23,6 @@
 angular.module('guacRadius').config(['formServiceProvider',
         function guacRadiusConfig(formServiceProvider) {
 
-    // Define field for the challenge from the RADIUS service
-    formServiceProvider.registerFieldType('GUAC_RADIUS_CHALLENGE_RESPONSE', {
-        module      : 'guacRadius',
-        controller  : 'radiusResponseController',
-        templateUrl : 'app/ext/radius/templates/radiusResponseField.html'
-    });
-
     // Define the hidden field for the RADIUS state
     formServiceProvider.registerFieldType('GUAC_RADIUS_STATE', {
         module      : 'guacRadius',
diff --git a/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js b/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js
deleted file mode 100644
index 4782b20..0000000
--- a/extensions/guacamole-auth-radius/src/main/resources/controllers/radiusResponseController.js
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one
- * or more contributor license agreements.  See the NOTICE file
- * distributed with this work for additional information
- * regarding copyright ownership.  The ASF licenses this file
- * to you under the Apache License, Version 2.0 (the
- * "License"); you may not use this file except in compliance
- * with the License.  You may obtain a copy of the License at
- *
- *   http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing,
- * software distributed under the License is distributed on an
- * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
- * KIND, either express or implied.  See the License for the
- * specific language governing permissions and limitations
- * under the License.
- */
-
-/**
- * Controller for the "GUAC_RADIUS_CHALLENGE_RESPONSE" field which
- * passes the RADIUS server challenge to the user and takes the response.
- */
-angular.module('guacRadius').controller('radiusResponseController', ['$scope', '$injector',
-        function radiusResponseController($scope, $injector) {
-
-    // Populate the reply message field
-    $scope.radiusPlaceholder = $scope.field.challenge;
-
-}]);
diff --git a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
index 55a3a0c..707f233 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-radius/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "RADIUS Authentication Backend",
     "namespace" : "radius",
@@ -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..1ba2623 100644
--- a/extensions/guacamole-auth-radius/src/main/resources/translations/en.json
+++ b/extensions/guacamole-auth-radius/src/main/resources/translations/en.json
@@ -5,9 +5,8 @@
     },
 
     "LOGIN" : {
-        "FIELD_HEADER_GUAC_RADIUS_CHALLENGE_RESPONSE" : "",
         "FIELD_HEADER_GUAC_RADIUS_STATE"              : "",
-        "INFO_RADIUS_ADDL_REQUIRED"                   : "Please supply additional credentials"
+        "FIELD_HEADER_RADIUSCHALLENGE"                : ""
     }
 
 }
diff --git a/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/pom.xml b/extensions/guacamole-auth-totp/pom.xml
index d34db07..180cbde 100644
--- a/extensions/guacamole-auth-totp/pom.xml
+++ b/extensions/guacamole-auth-totp/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-auth-totp</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-auth-totp</name>
     <url>http://guacamole.incubator.apache.org/</url>
 
@@ -217,9 +217,16 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>provided</scope>
         </dependency>
+        
+        <!-- Guava - Utility Library -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <version>27.0.1-jre</version>
+        </dependency>
 
         <!-- Guice -->
         <dependency>
@@ -241,13 +248,6 @@
             <scope>provided</scope>
         </dependency>
 
-        <!-- Guava - Utility Library -->
-        <dependency>
-            <groupId>com.google.guava</groupId>
-            <artifactId>guava</artifactId>
-            <version>18.0</version>
-        </dependency>
-
         <!-- JUnit -->
         <dependency>
             <groupId>junit</groupId>
diff --git a/extensions/guacamole-auth-totp/src/licenses/LICENSE b/extensions/guacamole-auth-totp/src/licenses/LICENSE
index 8a66d23..4607964 100644
--- a/extensions/guacamole-auth-totp/src/licenses/LICENSE
+++ b/extensions/guacamole-auth-totp/src/licenses/LICENSE
@@ -232,10 +232,10 @@
 Guava: Google Core Libraries for Java (https://github.com/google/guava)
 -----------------------------------------------------------------------
 
-    Version: 18.0
+    Version: 27.0.1-jre
     From: 'Google Inc.' (http://www.google.com/)
     License(s):
-        Apache v2.0 (bundled/guava-18.0/COPYING)
+        Apache v2.0 (bundled/guava-27.0.1-jre/COPYING)
 
 
 JSR-330 / Dependency Injection for Java (http://code.google.com/p/atinject/)
diff --git a/extensions/guacamole-auth-totp/src/licenses/NOTICE b/extensions/guacamole-auth-totp/src/licenses/NOTICE
index 97e6130..39de3ec 100644
--- a/extensions/guacamole-auth-totp/src/licenses/NOTICE
+++ b/extensions/guacamole-auth-totp/src/licenses/NOTICE
@@ -1,5 +1,5 @@
 Apache Guacamole
-Copyright 2018 The Apache Software Foundation
+Copyright 2019 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/extensions/guacamole-auth-totp/src/licenses/bundled/guava-18.0/COPYING b/extensions/guacamole-auth-totp/src/licenses/bundled/guava-27.0.1-jre/COPYING
similarity index 100%
rename from extensions/guacamole-auth-totp/src/licenses/bundled/guava-18.0/COPYING
rename to extensions/guacamole-auth-totp/src/licenses/bundled/guava-27.0.1-jre/COPYING
diff --git a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/form/AuthenticationCodeField.java b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/form/AuthenticationCodeField.java
index 1a61e89..58b6527 100644
--- a/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/form/AuthenticationCodeField.java
+++ b/extensions/guacamole-auth-totp/src/main/java/org/apache/guacamole/auth/totp/form/AuthenticationCodeField.java
@@ -30,7 +30,6 @@
 import java.io.IOException;
 import java.net.URI;
 import javax.ws.rs.core.UriBuilder;
-import javax.xml.bind.DatatypeConverter;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.auth.totp.user.UserTOTPKey;
 import org.apache.guacamole.auth.totp.conf.ConfigurationService;
@@ -309,7 +308,7 @@
 
         // Return data URI for generated image
         return "data:image/png;base64,"
-                + DatatypeConverter.printBase64Binary(stream.toByteArray());
+                + BaseEncoding.base64().encode(stream.toByteArray());
 
     }
 
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 fd62e14..ed42d6f 100644
--- a/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-totp/src/main/resources/guac-manifest.json
@@ -1,6 +1,6 @@
 {
 
-    "guacamoleVersion" : "1.0.0",
+    "guacamoleVersion" : "1.1.0",
 
     "name"      : "TOTP TFA Authentication Backend",
     "namespace" : "totp",
@@ -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/pom.xml b/guacamole-common-js/pom.xml
index cf9c45d..e94271b 100644
--- a/guacamole-common-js/pom.xml
+++ b/guacamole-common-js/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-common-js</artifactId>
     <packaging>pom</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-common-js</name>
     <url>http://guacamole.apache.org/</url>
 
diff --git a/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js
index 85e9346..07baf77 100644
--- a/guacamole-common-js/src/main/webapp/modules/Client.js
+++ b/guacamole-common-js/src/main/webapp/modules/Client.js
@@ -448,6 +448,31 @@
     };
 
     /**
+     * Opens a new argument value stream for writing, having the given
+     * parameter name and mimetype, requesting that the connection parameter
+     * with the given name be updated to the value described by the contents
+     * of the following stream. The instruction necessary to create this stream
+     * will automatically be sent.
+     *
+     * @param {String} mimetype
+     *     The mimetype of the data being sent.
+     *
+     * @param {String} name
+     *     The name of the connection parameter to attempt to update.
+     *
+     * @return {Guacamole.OutputStream}
+     *     The created argument value stream.
+     */
+    this.createArgumentValueStream = function createArgumentValueStream(mimetype, name) {
+
+        // Allocate and associate stream with argument value metadata
+        var stream = guac_client.createOutputStream();
+        tunnel.sendMessage("argv", stream.index, mimetype, name);
+        return stream;
+
+    };
+
+    /**
      * Creates a new output stream associated with the given object and having
      * the given mimetype and name. The legality of a mimetype and name is
      * dictated by the object itself. The instruction necessary to create this
@@ -625,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
@@ -821,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-js/src/main/webapp/modules/Version.js b/guacamole-common-js/src/main/webapp/modules/Version.js
index 737d8d2..9131165 100644
--- a/guacamole-common-js/src/main/webapp/modules/Version.js
+++ b/guacamole-common-js/src/main/webapp/modules/Version.js
@@ -27,4 +27,4 @@
  *
  * @type {String}
  */
-Guacamole.API_VERSION = "1.0.0";
+Guacamole.API_VERSION = "1.1.0";
diff --git a/guacamole-common/pom.xml b/guacamole-common/pom.xml
index 219d694..2b4f353 100644
--- a/guacamole-common/pom.xml
+++ b/guacamole-common/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-common</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-common</name>
     <url>http://guacamole.apache.org/</url>
 
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java
index cf43b68..fe4efca 100644
--- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/ConfiguredGuacamoleSocket.java
@@ -19,7 +19,6 @@
 
 package org.apache.guacamole.protocol;
 
-
 import java.util.List;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
@@ -57,6 +56,15 @@
     private String id;
     
     /**
+     * The protocol version that will be used to communicate with guacd.  The
+     * default is 1.0.0, and, if the server does not provide a specific version
+     * it will be assumed that it operates at this version and certain features
+     * may be unavailable.
+     */
+    private GuacamoleProtocolVersion protocolVersion =
+            GuacamoleProtocolVersion.VERSION_1_0_0;
+    
+    /**
      * Waits for the instruction having the given opcode, returning that
      * instruction once it has been read. If the instruction is never read,
      * an exception is thrown.
@@ -142,6 +150,23 @@
 
             // Retrieve argument name
             String arg_name = arg_names.get(i);
+            
+            // Check for valid protocol version as first argument
+            if (i == 0) {
+                GuacamoleProtocolVersion version = GuacamoleProtocolVersion.parseVersion(arg_name);
+                if (version != null) {
+
+                    // Use the lowest common version supported
+                    if (version.atLeast(GuacamoleProtocolVersion.LATEST))
+                        version = GuacamoleProtocolVersion.LATEST;
+
+                    // Respond with the version selected
+                    arg_values[i] = version.toString();
+                    protocolVersion = version;
+                    continue;
+
+                }
+            }
 
             // Get defined value for name
             String value = config.getParameter(arg_name);
@@ -184,6 +209,13 @@
                     "image",
                     info.getImageMimetypes().toArray(new String[0])
                 ));
+        
+        // Send client timezone, if supported and available
+        if (GuacamoleProtocolCapability.TIMEZONE_HANDSHAKE.isSupported(protocolVersion)) {
+            String timezone = info.getTimezone();
+            if (timezone != null)
+                writer.writeInstruction(new GuacamoleInstruction("timezone", info.getTimezone()));
+        }
 
         // Send args
         writer.writeInstruction(new GuacamoleInstruction("connect", arg_values));
@@ -221,6 +253,20 @@
         return id;
     }
 
+    /**
+     * Returns the version of the Guacamole protocol associated with the
+     * Guacamole connection negotiated by this ConfiguredGuacamoleSocket. This
+     * version is the lowest version common to both ConfiguredGuacamoleSocket
+     * and the relevant Guacamole proxy instance (guacd).
+     *
+     * @return
+     *     The protocol version that this ConfiguredGuacamoleSocket will use to
+     *     communicate with guacd.
+     */
+    public GuacamoleProtocolVersion getProtocolVersion() {
+        return protocolVersion;
+    }
+
     @Override
     public GuacamoleWriter getWriter() {
         return socket.getWriter();
diff --git a/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java b/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java
index 627c9fb..3c64c51 100644
--- a/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java
+++ b/guacamole-common/src/main/java/org/apache/guacamole/protocol/FailoverGuacamoleSocket.java
@@ -49,11 +49,10 @@
             LoggerFactory.getLogger(FailoverGuacamoleSocket.class);
 
     /**
-     * The maximum number of characters of Guacamole instruction data to store
-     * within the instruction queue while searching for errors. Once this limit
-     * is exceeded, the connection is assumed to be successful.
+     * The default maximum number of characters of Guacamole instruction data
+     * to store if no explicit limit is provided.
      */
-    private static final int INSTRUCTION_QUEUE_LIMIT = 2048;
+    private static final int DEFAULT_INSTRUCTION_QUEUE_LIMIT = 131072;
 
     /**
      * The wrapped socket being used.
@@ -131,9 +130,11 @@
     /**
      * Creates a new FailoverGuacamoleSocket which reads Guacamole instructions
      * from the given socket, searching for errors from the upstream remote
-     * desktop. If an upstream error is encountered, it is thrown as a
+     * desktop until the given instruction queue limit is reached. If an
+     * upstream error is encountered, it is thrown as a
      * GuacamoleUpstreamException. This constructor will block until an error
-     * is encountered, or until the connection appears to have been successful.
+     * is encountered, until insufficient space remains in the instruction
+     * queue, or until the connection appears to have been successful.
      * Once the FailoverGuacamoleSocket has been created, all reads, writes,
      * etc. will be delegated to the provided socket.
      *
@@ -141,6 +142,11 @@
      *     The GuacamoleSocket of the Guacamole connection this
      *     FailoverGuacamoleSocket should handle.
      *
+     * @param instructionQueueLimit
+     *     The maximum number of characters of Guacamole instruction data to
+     *     store within the instruction queue while searching for errors. Once
+     *     this limit is exceeded, the connection is assumed to be successful.
+     *
      * @throws GuacamoleException
      *     If an error occurs while reading data from the provided socket.
      *
@@ -148,7 +154,8 @@
      *     If the connection to guacd succeeded, but an error occurred while
      *     connecting to the remote desktop.
      */
-    public FailoverGuacamoleSocket(GuacamoleSocket socket)
+    public FailoverGuacamoleSocket(GuacamoleSocket socket,
+            final int instructionQueueLimit)
             throws GuacamoleException, GuacamoleUpstreamException {
 
         int totalQueueSize = 0;
@@ -177,7 +184,7 @@
             // Otherwise, track total data parsed, and assume connection is
             // successful if no error encountered within reasonable space
             totalQueueSize += instruction.toString().length();
-            if (totalQueueSize >= INSTRUCTION_QUEUE_LIMIT)
+            if (totalQueueSize >= instructionQueueLimit)
                 break;
 
         }
@@ -187,6 +194,33 @@
     }
 
     /**
+     * Creates a new FailoverGuacamoleSocket which reads Guacamole instructions
+     * from the given socket, searching for errors from the upstream remote
+     * desktop until a maximum of 128KB of instruction data has been queued. If
+     * an upstream error is encountered, it is thrown as a
+     * GuacamoleUpstreamException. This constructor will block until an error
+     * is encountered, until insufficient space remains in the instruction
+     * queue, or until the connection appears to have been successful.
+     * Once the FailoverGuacamoleSocket has been created, all reads, writes,
+     * etc. will be delegated to the provided socket.
+     *
+     * @param socket
+     *     The GuacamoleSocket of the Guacamole connection this
+     *     FailoverGuacamoleSocket should handle.
+     *
+     * @throws GuacamoleException
+     *     If an error occurs while reading data from the provided socket.
+     *
+     * @throws GuacamoleUpstreamException
+     *     If the connection to guacd succeeded, but an error occurred while
+     *     connecting to the remote desktop.
+     */
+    public FailoverGuacamoleSocket(GuacamoleSocket socket)
+            throws GuacamoleException, GuacamoleUpstreamException {
+        this(socket, DEFAULT_INSTRUCTION_QUEUE_LIMIT);
+    }
+
+    /**
      * GuacamoleReader which reads instructions from the queue populated when
      * the FailoverGuacamoleSocket was constructed. Once the queue has been
      * emptied, reads are delegated directly to the reader of the wrapped
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/README.md b/guacamole-docker/README.md
index 89ea1fa..34124b1 100644
--- a/guacamole-docker/README.md
+++ b/guacamole-docker/README.md
@@ -28,6 +28,29 @@
 `-p 8080:8080` option to expose this port at the level of the machine hosting
 Docker, as well.
 
+Docker Secrets
+==============
+The string `_FILE` may be appended to some of the environment variables listed 
+below if you are using MySQL or PostgreSQL authentication. This will cause the 
+startup script to load the values for those variables from files within
+the container. 
+
+This is useful for specifying sensitive info, ie. passwords for 
+the database, in secured files instead of plaintext environment variables. This
+is generally used for loading values from [Docker secrets](https://docs.docker.com/engine/swarm/secrets/#read-more-about-docker-secret-commands), which are stored at 
+`/run/secrets/<secret_name>` within the container.
+
+It is important to note that the startup script is configured such that:
+
+1. You may mix the use of Docker secrets and normal environment variables. 
+For example, you may wish to use `MYSQL_USER_FILE` and `MYSQL_PASSWORD_FILE`, 
+but wish to specify the database name with `MYSQL_DATABASE`
+
+2. If both a normal environment variable and its corresponding secret are defined
+in the same command line, or section within a [Compose](https://docs.docker.com/compose/) file, 
+the secret will take precedence. For instance, if both `MYSQL_PASSWORD`
+and `MYSQL_PASSWORD_FILE` are given, `MYSQL_PASSWORD_FILE` will be used.
+
 Deploying Guacamole with PostgreSQL authentication
 --------------------------------------------------
 
@@ -35,7 +58,10 @@
         --link some-postgres:postgres      \
         -e POSTGRES_DATABASE=guacamole_db  \
         -e POSTGRES_USER=guacamole_user    \
-        -e POSTGRES_PASSWORD=some_password \
+        -e POSTGRES_PASSWORD=some_password \        
+        -e POSTGRES_DATABASE_FILE=/run/secrets/<secret_name> \
+        -e POSTGRES_USER_FILE=/run/secrets/<secret_name> \
+        -e POSTGRES_PASSWORD_FILE=/run/secrets/<secret_name> \
         -d -p 8080:8080 guacamole/guacamole
 
 Linking Guacamole to PostgreSQL requires three environment variables. If any of
@@ -44,7 +70,14 @@
 
 1. `POSTGRES_DATABASE` - The name of the database to use for Guacamole authentication.
 2. `POSTGRES_USER` - The user that Guacamole will use to connect to PostgreSQL.
-3. `POSTGRES_PASSWORD` - The password that Guacamole will provide when connecting to PostgreSQL as `POSTGRES_USER`.
+3. `POSTGRES_PASSWORD` - The password that Guacamole will provide when connecting to 
+PostgreSQL as `POSTGRES_USER`.
+4. `POSTGRES_DATABASE_FILE` - The path of the docker secret containing the name of database 
+to use for Guacamole authentication.
+5. `POSTGRES_USER_FILE` - The path of the docker secret containing the name of the 
+user that Guacamole will use to connect to PostgreSQL.
+6. `POSTGRES_PASSWORD_FILE` - The path of the docker secret containing the password 
+that Guacamole will provide when connecting to PostgreSQL as `POSTGRES_USER.
 
 ### Initializing the PostgreSQL database
 
@@ -81,6 +114,9 @@
         -e MYSQL_DATABASE=guacamole_db  \
         -e MYSQL_USER=guacamole_user    \
         -e MYSQL_PASSWORD=some_password \
+        -e MYSQL_DATABASE_FILE=/run/secrets/<secret_name> \
+        -e MYSQL_USER_FILE=/run/secrets/<secret_name> \
+        -e MYSQL_PASSWORD_FILE=/run/secrets/<secret_name> \
         -d -p 8080:8080 guacamole/guacamole
 
 Linking Guacamole to MySQL requires three environment variables. If any of
@@ -89,7 +125,14 @@
 
 1. `MYSQL_DATABASE` - The name of the database to use for Guacamole authentication.
 2. `MYSQL_USER` - The user that Guacamole will use to connect to MySQL.
-3. `MYSQL_PASSWORD` - The password that Guacamole will provide when connecting to MySQL as `MYSQL_USER`.
+3. `MYSQL_PASSWORD` - The password that Guacamole will provide when connecting 
+to MySQL as `MYSQL_USER`.
+4. `MYSQL_DATABASE_FILE` - The path of the docker secret containing the name of the 
+database to use for Guacamole authentication.
+5. `MYSQL_USER_FILE` - The path of the docker secret containing the name of the user 
+that Guacamole will use to connect to MySQL.
+6. `MYSQL_PASSWORD_FILE` - The path of the docker secret containing the password 
+that Guacamole will provide when connecting to MySQL as`MYSQL_USER`.
 
 ### Initializing the MySQL database
 
diff --git a/guacamole-docker/bin/build-guacamole.sh b/guacamole-docker/bin/build-guacamole.sh
index 79115df..88087e5 100755
--- a/guacamole-docker/bin/build-guacamole.sh
+++ b/guacamole-docker/bin/build-guacamole.sh
@@ -38,9 +38,15 @@
 ##     subdirectories within this directory, and files will thus be grouped by
 ##     extension type.
 ##
+## @param BUILD_PROFILE
+##     The build profile that will be passed to Maven build process. Defaults
+##     to empty string. Can be set to "lgpl-extensions" to e.g. include
+##     RADIUS authentication extension.
+##
 
 BUILD_DIR="$1"
 DESTINATION="$2"
+BUILD_PROFILE="$3"
 
 #
 # Create destination, if it does not yet exist
@@ -53,7 +59,12 @@
 #
 
 cd "$BUILD_DIR"
-mvn package
+
+if [ -z "$BUILD_PROFILE" ]; then
+    mvn package
+else
+    mvn -P "$BUILD_PROFILE" package
+fi
 
 #
 # Copy guacamole.war to destination
@@ -107,3 +118,34 @@
     "*.jar"                                             \
     "*.ldif"
 
+#
+# Copy Radius auth extension if it was build
+#
+
+if [ -f extensions/guacamole-auth-radius/target/guacamole-auth-radius*.jar ]; then
+    mkdir -p "$DESTINATION/radius"
+    cp extensions/guacamole-auth-radius/target/guacamole-auth-radius*.jar "$DESTINATION/radius"
+fi
+
+# Copy OPENID auth extension and schema modifications
+#
+
+if [ -f extensions/guacamole-auth-openid/target/guacamole-auth-openid*.jar ]; then
+    mkdir -p "$DESTINATION/openid"
+    cp extensions/guacamole-auth-openid/target/guacamole-auth-openid*.jar "$DESTINATION/openid"
+fi
+
+#
+# Copy Duo auth extension if it was built
+#
+
+if [ -f extensions/guacamole-auth-duo/target/*.tar.gz ]; then
+    mkdir -p "$DESTINATION/duo"
+    tar -xzf extensions/guacamole-auth-duo/target/*.tar.gz \
+        -C "$DESTINATION/duo/"                             \
+        --wildcards                                        \
+        --no-anchored                                      \
+        --no-wildcards-match-slash                         \
+        --strip-components=1                               \
+        "*.jar"
+fi
diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh
index 6309265..be97901 100755
--- a/guacamole-docker/bin/start.sh
+++ b/guacamole-docker/bin/start.sh
@@ -84,6 +84,29 @@
 
 }
 
+# Print error message regarding missing required variables for MySQL authentication
+mysql_missing_vars() {
+   cat <<END
+FATAL: Missing required environment variables
+-------------------------------------------------------------------------------
+If using a MySQL database, you must provide each of the following
+environment variables or their corresponding Docker secrets by appending _FILE
+to the environment variable, and setting the value to the path of the 
+corresponding secret:
+
+    MYSQL_USER         The user to authenticate as when connecting to
+                       MySQL.
+
+    MYSQL_PASSWORD     The password to use when authenticating with MySQL as
+                       MYSQL_USER.
+
+    MYSQL_DATABASE     The name of the MySQL database to use for Guacamole
+                       authentication.
+END
+    exit 1;
+}
+
+
 ##
 ## Adds properties to guacamole.properties which select the MySQL
 ## authentication provider, and configure it to connect to the linked MySQL
@@ -125,32 +148,38 @@
         exit 1;
     fi
 
-    # Verify required parameters are present
-    if [ -z "$MYSQL_USER" -o -z "$MYSQL_PASSWORD" -o -z "$MYSQL_DATABASE" ]; then
-        cat <<END
-FATAL: Missing required environment variables
--------------------------------------------------------------------------------
-If using a MySQL database, you must provide each of the following
-environment variables:
 
-    MYSQL_USER         The user to authenticate as when connecting to
-                       MySQL.
+    # Verify that the required Docker secrets are present, else, default to their normal environment variables
+    if [ -n "$MYSQL_USER_FILE" ]; then
+        set_property "mysql-username" `cat $MYSQL_USER_FILE`
+    elif [ -n "$MYSQL_USER" ]; then
+        set_property "mysql-username" "$MYSQL_USER"
+    else
+        mysql_missing_vars
+        exit 1;
+    fi
+    
+    if [ -n "$MYSQL_PASSWORD_FILE" ]; then
+        set_property "mysql-password" `cat $MYSQL_PASSWORD_FILE`
+    elif [ -n "$MYSQL_PASSWORD" ]; then
+        set_property "mysql-password" "$MYSQL_PASSWORD"
+    else
+        mysql_missing_vars
+        exit 1;
+    fi
 
-    MYSQL_PASSWORD     The password to use when authenticating with MySQL as
-                       MYSQL_USER.
-
-    MYSQL_DATABASE     The name of the MySQL database to use for Guacamole
-                       authentication.
-END
+    if [ -n "$MYSQL_DATABASE_FILE" ]; then
+        set_property "mysql-database" `cat $MYSQL_DATABASE_FILE`
+    elif [ -n "$MYSQL_DATABASE" ]; then
+        set_property "mysql-database" "$MYSQL_DATABASE"
+    else
+        mysql_missing_vars
         exit 1;
     fi
 
     # Update config file
     set_property "mysql-hostname" "$MYSQL_HOSTNAME"
     set_property "mysql-port"     "$MYSQL_PORT"
-    set_property "mysql-database" "$MYSQL_DATABASE"
-    set_property "mysql-username" "$MYSQL_USER"
-    set_property "mysql-password" "$MYSQL_PASSWORD"
 
     set_optional_property               \
         "mysql-absolute-max-connections" \
@@ -178,6 +207,28 @@
 
 }
 
+# Print error message regarding missing required variables for PostgreSQL authentication
+postgres_missing_vars() {
+    cat <<END
+FATAL: Missing required environment variables
+-------------------------------------------------------------------------------
+If using a PostgreSQL database, you must provide each of the following
+environment variables or their corresponding Docker secrets by appending _FILE
+to the environment variable, and setting the value to the path of the 
+corresponding secret:
+
+    POSTGRES_USER      The user to authenticate as when connecting to
+                       PostgreSQL.
+
+    POSTGRES_PASSWORD  The password to use when authenticating with PostgreSQL
+                       as POSTGRES_USER.
+
+    POSTGRES_DATABASE  The name of the PostgreSQL database to use for Guacamole
+                       authentication.
+END
+    exit 1;
+}
+
 ##
 ## Adds properties to guacamole.properties which select the PostgreSQL
 ## authentication provider, and configure it to connect to the linked
@@ -221,32 +272,37 @@
         exit 1;
     fi
 
-    # Verify required parameters are present
-    if [ -z "$POSTGRES_USER" -o -z "$POSTGRES_PASSWORD" -o -z "$POSTGRES_DATABASE" ]; then
-        cat <<END
-FATAL: Missing required environment variables
--------------------------------------------------------------------------------
-If using a PostgreSQL database, you must provide each of the following
-environment variables:
+    # Verify that the required Docker secrets are present, else, default to their normal environment variables
+    if [ -n "$POSTGRES_USER_FILE" ]; then
+        set_property "postgresql-username" `cat $POSTGRES_USER_FILE`
+    elif [ -n "$POSTGRES_USER" ]; then
+        set_property "postgresql-username" "$POSTGRES_USER"
+    else
+        postgres_missing_vars
+        exit 1;
+    fi
+    
+    if [ -n "$POSTGRES_PASSWORD_FILE" ]; then
+        set_property "postgresql-password" `cat $POSTGRES_PASSWORD_FILE`
+    elif [ -n "$POSTGRES_PASSWORD" ]; then
+        set_property "postgresql-password" "$POSTGRES_PASSWORD"
+    else
+        postgres_missing_vars
+        exit 1;
+    fi
 
-    POSTGRES_USER      The user to authenticate as when connecting to
-                       PostgreSQL.
-
-    POSTGRES_PASSWORD  The password to use when authenticating with PostgreSQL
-                       as POSTGRES_USER.
-
-    POSTGRES_DATABASE  The name of the PostgreSQL database to use for Guacamole
-                       authentication.
-END
+    if [ -n "$POSTGRES_DATABASE_FILE" ]; then
+        set_property "postgresql-database" `cat $POSTGRES_DATABASE_FILE`
+    elif [ -n "$POSTGRES_DATABASE" ]; then
+        set_property "postgresql-database" "$POSTGRES_DATABASE"
+    else
+        postgres_missing_vars
         exit 1;
     fi
 
     # Update config file
     set_property "postgresql-hostname" "$POSTGRES_HOSTNAME"
     set_property "postgresql-port"     "$POSTGRES_PORT"
-    set_property "postgresql-database" "$POSTGRES_DATABASE"
-    set_property "postgresql-username" "$POSTGRES_USER"
-    set_property "postgresql-password" "$POSTGRES_PASSWORD"
 
     set_optional_property               \
         "postgresql-absolute-max-connections" \
@@ -303,25 +359,215 @@
     set_property          "ldap-hostname"           "$LDAP_HOSTNAME"
     set_optional_property "ldap-port"               "$LDAP_PORT"
     set_optional_property "ldap-encryption-method"  "$LDAP_ENCRYPTION_METHOD"
-    set_property          "ldap-user-base-dn"       "$LDAP_USER_BASE_DN"
-    set_optional_property "ldap-username-attribute" "$LDAP_USERNAME_ATTRIBUTE"
-    set_optional_property "ldap-group-base-dn"      "$LDAP_GROUP_BASE_DN"
-    set_optional_property "ldap-config-base-dn"     "$LDAP_CONFIG_BASE_DN"
-
-    set_optional_property     \
-        "ldap-search-bind-dn" \
-        "$LDAP_SEARCH_BIND_DN"
+    set_optional_property "ldap-max-search-results" "$LDAP_MAX_SEARCH_RESULTS"
+    set_optional_property "ldap-search-bind-dn"     "$LDAP_SEARCH_BIND_DN"
 
     set_optional_property           \
         "ldap-search-bind-password" \
         "$LDAP_SEARCH_BIND_PASSWORD"
 
+    set_property          "ldap-user-base-dn"       "$LDAP_USER_BASE_DN"
+    set_optional_property "ldap-username-attribute" "$LDAP_USERNAME_ATTRIBUTE"
+    set_optional_property "ldap-member-attribute"   "$LDAP_MEMBER_ATTRIBUTE"
+    set_optional_property "ldap-user-search-filter" "$LDAP_USER_SEARCH_FILTER"
+    set_optional_property "ldap-config-base-dn"     "$LDAP_CONFIG_BASE_DN"
+    set_optional_property "ldap-group-base-dn"      "$LDAP_GROUP_BASE_DN"
+
+    set_optional_property           \
+        "ldap-group-name-attribute" \
+        "$LDAP_GROUP_NAME_ATTRIBUTE"
+
+    set_optional_property           \
+        "ldap-dereference-aliases"  \
+        "$LDAP_DEREFERENCE_ALIASES"
+
+    set_optional_property "ldap-follow-referrals"   "$LDAP_FOLLOW_REFERRALS"
+    set_optional_property "ldap-max-referral-hops"  "$LDAP_MAX_REFERRAL_HOPS"
+    set_optional_property "ldap-operation-timeout"  "$LDAP_OPERATION_TIMEOUT"
+
     # Add required .jar files to GUACAMOLE_EXT
     ln -s /opt/guacamole/ldap/guacamole-auth-*.jar "$GUACAMOLE_EXT"
 
 }
 
 ##
+## Adds properties to guacamole.properties which select the LDAP
+## authentication provider, and configure it to connect to the specified LDAP
+## directory.
+##
+associate_radius() {
+
+    # Verify required parameters are present
+    if [ -z "$RADIUS_SHARED_SECRET" -o -z "$RADIUS_AUTH_PROTOCOL" ]; then
+        cat <<END
+FATAL: Missing required environment variables
+-------------------------------------------------------------------------------
+If using RADIUS server, you must provide each of the following environment
+variables:
+
+    RADIUS_SHARED_SECRET   The shared secret to use when talking to the 
+                           RADIUS server.
+
+    RADIUS_AUTH_PROTOCOL   The authentication protocol to use when talking 
+                           to the RADIUS server.
+                           Supported values are: 
+                             pap, chap, mschapv1, mschapv2, eap-md5, 
+                             eap-tls and eap-ttls.
+END
+        exit 1;
+    fi
+
+    # Verify provided files do exist and are readable
+    if [ -n "$RADIUS_KEY_FILE" -a ! -r "$RADIUS_KEY_FILE" ]; then
+       cat <<END
+FATAL: Provided file RADIUS_KEY_FILE=$RADIUS_KEY_FILE does not exist 
+       or is not readable!
+-------------------------------------------------------------------------------
+If you provide key or CA files you need to mount those into the container and
+make sure they are readable for the user in the container.
+END
+        exit 1;
+    fi
+    if [ -n "$RADIUS_CA_FILE" -a ! -r "$RADIUS_CA_FILE" ]; then
+       cat <<END
+FATAL: Provided file RADIUS_CA_FILE=$RADIUS_CA_FILE does not exist 
+       or is not readable!
+-------------------------------------------------------------------------------
+If you provide key or CA files you need to mount those into the container and
+make sure they are readable for the user in the container.
+END
+        exit 1;
+    fi
+    if [ "$RADIUS_AUTH_PROTOCOL" = "eap-ttls" -a -z "$RADIUS_EAP_TTLS_INNER_PROTOCOL" ]; then
+       cat <<END
+FATAL: Authentication protocol "eap-ttls" specified but
+       RADIUS_EAP_TTLS_INNER_PROTOCOL is not set!
+-------------------------------------------------------------------------------
+When EAP-TTLS is used, this parameter specifies the inner (tunneled)
+protocol to use talking to the RADIUS server.
+END
+        exit 1;
+    fi
+
+    # Update config file
+    set_optional_property "radius-hostname"         "$RADIUS_HOSTNAME"
+    set_optional_property "radius-auth-port"        "$RADIUS_AUTH_PORT"
+    set_property          "radius-shared-secret"    "$RADIUS_SHARED_SECRET"
+    set_property          "radius-auth-protocol"    "$RADIUS_AUTH_PROTOCOL"
+    set_optional_property "radius-key-file"         "$RADIUS_KEY_FILE"
+    set_optional_property "radius-key-type"         "$RADIUS_KEY_TYPE"
+    set_optional_property "radius-key-password"     "$RADIUS_KEY_PASSWORD"
+    set_optional_property "radius-ca-file"          "$RADIUS_CA_FILE"
+    set_optional_property "radius-ca-type"          "$RADIUS_CA_TYPE"
+    set_optional_property "radius-ca-password"      "$RADIUS_CA_PASSWORD"
+    set_optional_property "radius-trust-all"        "$RADIUS_TRUST_ALL"
+    set_optional_property "radius-retries"          "$RADIUS_RETRIES"
+    set_optional_property "radius-timeout"          "$RADIUS_TIMEOUT"
+
+    set_optional_property \
+       "radius-eap-ttls-inner-protocol" \
+       "$RADIUS_EAP_TTLS_INNER_PROTOCOL"
+
+    # Add required .jar files to GUACAMOLE_EXT
+    ln -s /opt/guacamole/radius/guacamole-auth-*.jar "$GUACAMOLE_EXT"
+}
+
+## Adds properties to guacamole.properties which select the OPENID
+## authentication provider, and configure it to connect to the specified OPENID
+## provider.
+##
+associate_openid() {
+
+    # Verify required parameters are present
+    if [ -z "$OPENID_AUTHORIZATION_ENDPOINT" ] || \
+       [ -z "$OPENID_JWKS_ENDPOINT" ]          || \
+       [ -z "$OPENID_ISSUER" ]                 || \
+       [ -z "$OPENID_CLIENT_ID" ]              || \          
+       [ -z "$OPENID_REDIRECT_URI" ]
+    then
+        cat <<END
+FATAL: Missing required environment variables
+-------------------------------------------------------------------------------
+If using an openid authentication, you must provide each of the following
+environment variables:
+
+    OPENID_AUTHORIZATION_ENDPOINT   The authorization endpoint (URI) of the OpenID service.
+
+    OPENID_JWKS_ENDPOINT            The endpoint (URI) of the JWKS service which defines
+                                    how received ID tokens (JSON Web Tokens or JWTs) 
+                                    shall be validated.
+
+    OPENID_ISSUER                   The issuer to expect for all received ID tokens.
+
+    OPENID_CLIENT_ID                The OpenID client ID which should be submitted 
+                                    to the OpenID service when necessary. 
+                                    This value is typically provided to you by the OpenID 
+                                    service when OpenID credentials are generated for your application.
+
+    OPENID_REDIRECT_URI             The URI that should be submitted to the OpenID service such that 
+                                    they can redirect the authenticated user back to Guacamole after 
+                                    the authentication process is complete. This must be the full URL 
+                                    that a user would enter into their browser to access Guacamole.
+END
+        exit 1;
+    fi
+
+    # Update config file
+    set_property          "openid-authorization-endpoint"    "$OPENID_AUTHORIZATION_ENDPOINT"
+    set_property          "openid-jwks-endpoint"             "$OPENID_JWKS_ENDPOINT"
+    set_property          "openid-issuer"                    "$OPENID_ISSUER"
+    set_property          "openid-client-id"                 "$OPENID_CLIENT_ID"
+    set_property          "openid-redirect-uri"              "$OPENID_REDIRECT_URI"
+    set_optional_property "openid-username-claim-type"       "$OPENID_USERNAME_CLAIM_TYPE"
+
+    # Add required .jar files to GUACAMOLE_EXT
+    # "1-{}" make it sorted as a first provider (only authentication)
+    # so it can work together with the database providers (authorization)
+    find /opt/guacamole/openid/ -name "*.jar" | awk -F/ '{print $NF}' | \
+    xargs -I '{}' ln -s "/opt/guacamole/openid/{}" "${GUACAMOLE_EXT}/1-{}"
+
+}
+
+##
+## Adds properties to guacamole.properties which configure the Duo two-factor
+## authentication service. Checks to see if all variables are defined and makes sure
+## DUO_APPLICATION_KEY is >= 40 characters.
+##
+associate_duo() {
+    # Verify required parameters are present
+    if [ -z "$DUO_INTEGRATION_KEY" ]      || \
+       [ -z "$DUO_SECRET_KEY" ]           || \
+       [ ${#DUO_APPLICATION_KEY} -lt 40 ]
+    then
+        cat <<END
+FATAL: Missing required environment variables
+-------------------------------------------------------------------------------
+If using the Duo authentication extension, you must provide each of the 
+following environment variables:
+
+    DUO_API_HOSTNAME        The hostname of the Duo API endpoint.
+
+    DUO_INTEGRATION_KEY     The integration key provided for Guacamole by Duo.
+
+    DUO_SECRET_KEY          The secret key provided for Guacamole by Duo. 
+
+    DUO_APPLICATION_KEY     An arbitrary, random key.
+                            This value must be at least 40 characters.
+END
+        exit 1;
+    fi
+
+    # Update config file
+    set_property "duo-api-hostname"                 "$DUO_API_HOSTNAME"
+    set_property "duo-integration-key"              "$DUO_INTEGRATION_KEY"
+    set_property "duo-secret-key"                   "$DUO_SECRET_KEY"
+    set_property "duo-application-key"              "$DUO_APPLICATION_KEY"
+
+    # Add required .jar files to GUACAMOLE_EXT
+    ln -s /opt/guacamole/duo/guacamole-auth-*.jar   "$GUACAMOLE_EXT"
+}
+
+##
 ## Starts Guacamole under Tomcat, replacing the current process with the
 ## Tomcat process. As the current process will be replaced, this MUST be the
 ## last function run within the script.
@@ -329,7 +575,8 @@
 start_guacamole() {
 
     # Install webapp
-    ln -sf /opt/guacamole/guacamole.war /usr/local/tomcat/webapps/
+    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
     cd /usr/local/tomcat
@@ -406,13 +653,13 @@
 INSTALLED_AUTH=""
 
 # Use MySQL if database specified
-if [ -n "$MYSQL_DATABASE" ]; then
+if [ -n "$MYSQL_DATABASE" -o -n "$MYSQL_DATABASE_FILE" ]; then
     associate_mysql
     INSTALLED_AUTH="$INSTALLED_AUTH mysql"
 fi
 
 # Use PostgreSQL if database specified
-if [ -n "$POSTGRES_DATABASE" ]; then
+if [ -n "$POSTGRES_DATABASE" -o -n "$POSTGRES_DATABASE_FILE" ]; then
     associate_postgresql
     INSTALLED_AUTH="$INSTALLED_AUTH postgres"
 fi
@@ -423,6 +670,18 @@
     INSTALLED_AUTH="$INSTALLED_AUTH ldap"
 fi
 
+# Use RADIUS server if specified
+if [ -n "$RADIUS_SHARED_SECRET" ]; then
+    associate_radius
+    INSTALLED_AUTH="$INSTALLED_AUTH radius"
+fi
+
+# Use OPENID if specified
+if [ -n "$OPENID_AUTHORIZATION_ENDPOINT" ]; then
+    associate_openid
+    INSTALLED_AUTH="$INSTALLED_AUTH openid"
+fi
+
 #
 # Validate that at least one authentication backend is installed
 #
@@ -432,14 +691,25 @@
 FATAL: No authentication configured
 -------------------------------------------------------------------------------
 The Guacamole Docker container needs at least one authentication mechanism in
-order to function, such as a MySQL database, PostgreSQL database, or LDAP
-directory.  Please specify at least the MYSQL_DATABASE or POSTGRES_DATABASE
-environment variables, or check Guacamole's Docker documentation regarding
-configuring LDAP and/or custom extensions.
+order to function, such as a MySQL database, PostgreSQL database, LDAP
+directory or RADIUS server. Please specify at least the MYSQL_DATABASE or 
+POSTGRES_DATABASE environment variables, or check Guacamole's Docker 
+documentation regarding configuring LDAP and/or custom extensions.
 END
     exit 1;
 fi
 
+# Use Duo if specified.
+if [ -n "$DUO_API_HOSTNAME" ]; then
+    associate_duo
+fi
+
+# Set logback level if specified
+if [ -n "$LOGBACK_LEVEL" ]; then
+    unzip -o -j /opt/guacamole/guacamole.war WEB-INF/classes/logback.xml -d $GUACAMOLE_HOME
+    sed -i "s/level=\"info\"/level=\"$LOGBACK_LEVEL\"/" $GUACAMOLE_HOME/logback.xml
+fi
+
 #
 # Finally start Guacamole (under Tomcat)
 #
diff --git a/guacamole-ext/pom.xml b/guacamole-ext/pom.xml
index e123b9f..a4e7b07 100644
--- a/guacamole-ext/pom.xml
+++ b/guacamole-ext/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-ext</artifactId>
     <packaging>jar</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-ext</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -155,7 +155,7 @@
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <scope>compile</scope>
         </dependency>
 
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java b/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java
index 1b45c95..b1d4865 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/environment/LocalEnvironment.java
@@ -53,7 +53,7 @@
      * Array of all known protocol names.
      */
     private static final String[] KNOWN_PROTOCOLS = new String[]{
-        "vnc", "rdp", "ssh", "telnet"};
+        "vnc", "rdp", "ssh", "telnet", "kubernetes"};
 
     /**
      * The hostname to use when connecting to guacd if no hostname is provided
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java b/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java
index 19f1ead..d35facd 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/form/Field.java
@@ -92,6 +92,14 @@
         public static String TIMEZONE = "TIMEZONE";
 
         /**
+         * Field type which allows selection of languages. The languages
+         * displayed are the set of languages supported by the Guacamole web
+         * application. Legal values are valid language IDs, as dictated by
+         * the filenames of Guacamole's available translations.
+         */
+        public static String LANGUAGE = "LANGUAGE";
+
+        /**
          * A date field whose legal values conform to the pattern "YYYY-MM-DD",
          * zero-padded.
          */
@@ -109,6 +117,12 @@
          */
         public static String QUERY_PARAMETER = "QUERY_PARAMETER";
 
+        /**
+         * A color scheme accepted by the Guacamole server terminal emulator
+         * and protocols which leverage it.
+         */
+        public static String TERMINAL_COLOR_SCHEME = "TERMINAL_COLOR_SCHEME";
+
     }
 
     /**
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/form/LanguageField.java b/guacamole-ext/src/main/java/org/apache/guacamole/form/LanguageField.java
new file mode 100644
index 0000000..a87d772
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/form/LanguageField.java
@@ -0,0 +1,64 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.form;
+
+/**
+ * Represents a language field. The field may contain only valid language
+ * identifiers as used by the Guacamole web application for its translations.
+ * Language identifiers are defined by the filenames of the JSON files
+ * containing the translation.
+ */
+public class LanguageField extends Field {
+
+    /**
+     * Creates a new LanguageField with the given name.
+     *
+     * @param name
+     *     The unique name to associate with this field.
+     */
+    public LanguageField(String name) {
+        super(name, Field.Type.LANGUAGE);
+    }
+
+    /**
+     * Parses the given string into a language ID string. As any string may be
+     * a valid language ID as long as it has a corresponding translation, the
+     * only transformation currently performed by this function is to ensure
+     * that a blank language string is parsed into null.
+     *
+     * @param language
+     *     The language string to parse, which may be null.
+     *
+     * @return
+     *     The ID of the language corresponding to the given string, or null if
+     *     if the given language string was null or blank.
+     */
+    public static String parse(String language) {
+
+        // Return null if no language is provided
+        if (language == null || language.isEmpty())
+            return null;
+
+        // Otherwise, assume language is already a valid language ID
+        return language;
+
+    }
+
+}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/form/TerminalColorSchemeField.java b/guacamole-ext/src/main/java/org/apache/guacamole/form/TerminalColorSchemeField.java
new file mode 100644
index 0000000..ccd7ce5
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/form/TerminalColorSchemeField.java
@@ -0,0 +1,39 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.form;
+
+/**
+ * Represents a terminal color scheme field. The field may contain only valid
+ * terminal color schemes as used by the Guacamole server terminal emulator
+ * and protocols which leverage it (SSH, telnet, Kubernetes).
+ */
+public class TerminalColorSchemeField extends Field {
+
+    /**
+     * Creates a new TerminalColorSchemeField with the given name.
+     *
+     * @param name
+     *     The unique name to associate with this field.
+     */
+    public TerminalColorSchemeField(String name) {
+        super(name, Field.Type.TERMINAL_COLOR_SCHEME);
+    }
+
+}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/form/TimeZoneField.java b/guacamole-ext/src/main/java/org/apache/guacamole/form/TimeZoneField.java
index 9305410..90084e4 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/form/TimeZoneField.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/form/TimeZoneField.java
@@ -20,8 +20,9 @@
 package org.apache.guacamole.form;
 
 /**
- * Represents a time zone field. The field may contain only valid time zone IDs,
- * as dictated by TimeZone.getAvailableIDs().
+ * Represents a time zone field. The field may contain only valid time zone
+ * identifiers, as defined by the IANA time zone database. Such identifiers are
+ * also valid Java time zone IDs as dictated by TimeZone.getAvailableIDs().
  */
 public class TimeZoneField extends Field {
 
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java
index ad1d6d3..9bcce3e 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/ActiveConnection.java
@@ -20,13 +20,18 @@
 package org.apache.guacamole.net.auth;
 
 import java.util.Date;
+import java.util.Map;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleSecurityException;
 import org.apache.guacamole.net.GuacamoleTunnel;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
 
 /**
  * A pairing of username and GuacamoleTunnel representing an active usage of a
  * particular connection.
  */
-public interface ActiveConnection extends Identifiable, Shareable<SharingProfile> {
+public interface ActiveConnection extends Identifiable, Connectable,
+        Shareable<SharingProfile> {
 
     /**
      * Returns the identifier of the connection being actively used. Unlike the
@@ -136,5 +141,31 @@
      *     The connected GuacamoleTunnel, or null if permission is denied.
      */
     void setTunnel(GuacamoleTunnel tunnel);
+
+    /**
+     * Returns whether this ActiveConnection may be joined through a call to
+     * {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation, java.util.Map)}
+     * by the user that retrieved this ActiveConnection.
+     *
+     * @return
+     *     true if the user that retrieved this ActiveConnection may join the
+     *     ActiveConnection through a call to
+     *     {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation, java.util.Map)},
+     *     false otherwise.
+     */
+    default boolean isConnectable() {
+        return false;
+    }
+
+    @Override
+    default GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+        throw new GuacamoleSecurityException("Permission denied.");
+    }
+
+    @Override
+    default int getActiveConnections() {
+        return 0;
+    }
     
 }
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connectable.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connectable.java
index 7face92..2f3326f 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connectable.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/Connectable.java
@@ -19,6 +19,8 @@
 
 package org.apache.guacamole.net.auth;
 
+import java.util.Collections;
+import java.util.Map;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.net.GuacamoleTunnel;
 import org.apache.guacamole.protocol.GuacamoleClientInformation;
@@ -28,11 +30,30 @@
  */
 public interface Connectable {
 
+    /*
+     * IMPORTANT:
+     * ----------
+     * The web application (guacamole) defines its own version of this
+     * interface containing defaults which allow backwards compatibility with
+     * 1.0.0. Any changes to this interface MUST be properly reflected in that
+     * copy of the interface such that they are binary compatible.
+     */
+
     /**
      * Establishes a connection to guacd using the information associated with
      * this object. The connection will be provided the given client
      * information.
      *
+     * @deprecated
+     *     This function has been deprecated in favor of
+     *     {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation, java.util.Map)},
+     *     which allows for connection parameter tokens to be injected and
+     *     applied by cooperating extensions, replacing the functionality
+     *     previously provided through the {@link org.apache.guacamole.token.StandardTokens}
+     *     class. It continues to be defined on this interface for
+     *     compatibility. <strong>New implementations should instead implement
+     *     {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation, java.util.Map)}.</strong>
+     *
      * @param info
      *     Information associated with the connecting client.
      *
@@ -43,8 +64,39 @@
      *     If an error occurs while connecting to guacd, or if permission to
      *     connect is denied.
      */
-    public GuacamoleTunnel connect(GuacamoleClientInformation info)
-            throws GuacamoleException;
+    @Deprecated
+    default GuacamoleTunnel connect(GuacamoleClientInformation info)
+            throws GuacamoleException {
+        return this.connect(info, Collections.emptyMap());
+    }
+
+    /**
+     * Establishes a connection to guacd using the information associated with
+     * this object. The connection will be provided the given client
+     * information. Implementations which support parameter tokens should
+     * apply the given tokens when configuring the connection, such as with a
+     * {@link org.apache.guacamole.token.TokenFilter}.
+     *
+     * @see <a href="http://guacamole.apache.org/doc/gug/configuring-guacamole.html#parameter-tokens">Parameter Tokens</a>
+     *
+     * @param info
+     *     Information associated with the connecting client.
+     *
+     * @param tokens
+     *     A Map containing the token names and corresponding values to be
+     *     applied as parameter tokens when establishing the connection. If the
+     *     implementation does not support parameter tokens, this Map may be
+     *     ignored.
+     *
+     * @return
+     *     A fully-established GuacamoleTunnel.
+     *
+     * @throws GuacamoleException
+     *     If an error occurs while connecting to guacd, or if permission to
+     *     connect is denied.
+     */
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException;
 
     /**
      * Returns the number of active connections associated with this object.
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnection.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnection.java
index fa1ab78..95b6e93 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnection.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnection.java
@@ -19,6 +19,7 @@
 
 package org.apache.guacamole.net.auth;
 
+import java.util.Collections;
 import java.util.Date;
 import java.util.List;
 import java.util.Map;
@@ -40,6 +41,24 @@
     private final Connection connection;
 
     /**
+     * The tokens which should apply strictly to the next call to
+     * {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation)}.
+     * This storage is intended as a temporary bridge allowing the old version
+     * of connect() to be overridden while still resulting in the same behavior
+     * as older versions of DelegatingConnection. <strong>This storage should be
+     * removed once support for the old, deprecated connect() is removed.</strong>
+     */
+    private final ThreadLocal<Map<String, String>> currentTokens =
+            new ThreadLocal<Map<String, String>>() {
+
+        @Override
+        protected Map<String, String> initialValue() {
+            return Collections.emptyMap();
+        }
+
+    };
+
+    /**
      * Wraps the given Connection such that all function calls against this
      * DelegatingConnection will be delegated to it.
      *
@@ -128,9 +147,26 @@
     }
 
     @Override
+    @Deprecated
     public GuacamoleTunnel connect(GuacamoleClientInformation info)
             throws GuacamoleException {
-        return connection.connect(info);
+        return connection.connect(info, currentTokens.get());
+    }
+
+    @Override
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+
+        // Make received tokens available within the legacy connect() strictly
+        // in context of the current connect() call
+        try {
+            currentTokens.set(tokens);
+            return connect(info);
+        }
+        finally {
+            currentTokens.remove();
+        }
+
     }
 
     @Override
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnectionGroup.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnectionGroup.java
index db647d6..5af6eb1 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnectionGroup.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/DelegatingConnectionGroup.java
@@ -19,6 +19,7 @@
 
 package org.apache.guacamole.net.auth;
 
+import java.util.Collections;
 import java.util.Map;
 import java.util.Set;
 import org.apache.guacamole.GuacamoleException;
@@ -37,6 +38,25 @@
     private final ConnectionGroup connectionGroup;
 
     /**
+     * The tokens which should apply strictly to the next call to
+     * {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation)}.
+     * This storage is intended as a temporary bridge allowing the old version
+     * of connect() to be overridden while still resulting in the same behavior
+     * as older versions of DelegatingConnectionGroup. <strong>This storage
+     * should be removed once support for the old, deprecated connect() is
+     * removed.</strong>
+     */
+    private final ThreadLocal<Map<String, String>> currentTokens =
+            new ThreadLocal<Map<String, String>>() {
+
+        @Override
+        protected Map<String, String> initialValue() {
+            return Collections.emptyMap();
+        }
+
+    };
+
+    /**
      * Wraps the given ConnectionGroup such that all function calls against this
      * DelegatingConnectionGroup will be delegated to it.
      *
@@ -119,8 +139,26 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException {
-        return connectionGroup.connect(info);
+    @Deprecated
+    public GuacamoleTunnel connect(GuacamoleClientInformation info)
+            throws GuacamoleException {
+        return connectionGroup.connect(info, currentTokens.get());
+    }
+
+    @Override
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+
+        // Make received tokens available within the legacy connect() strictly
+        // in context of the current connect() call
+        try {
+            currentTokens.set(tokens);
+            return connect(info);
+        }
+        finally {
+            currentTokens.remove();
+        }
+
     }
 
     @Override
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/TokenInjectingConnection.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/TokenInjectingConnection.java
new file mode 100644
index 0000000..8a826d8
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/TokenInjectingConnection.java
@@ -0,0 +1,69 @@
+/*
+ * 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.net.auth;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.GuacamoleTunnel;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
+
+/**
+ * Connection implementation which overrides the connect() function of an
+ * underlying Connection, adding a given set of parameter tokens to the tokens
+ * already supplied.
+ */
+public class TokenInjectingConnection extends DelegatingConnection {
+
+    /**
+     * The additional tokens to include with each call to connect().
+     */
+    private final Map<String, String> tokens;
+
+    /**
+     * Wraps the given Connection, automatically adding the given tokens to
+     * each invocation of connect(). Any additional tokens which have the same
+     * name as existing tokens will override the existing values.
+     *
+     * @param connection
+     *     The Connection to wrap.
+     *
+     * @param tokens
+     *     The additional tokens to include with each call to connect().
+     */
+    public TokenInjectingConnection(Connection connection,
+            Map<String, String> tokens) {
+        super(connection);
+        this.tokens = tokens;
+    }
+
+    @Override
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+
+        // Apply provided tokens over those given to connect()
+        tokens = new HashMap<>(tokens);
+        tokens.putAll(this.tokens);
+
+        return super.connect(info, tokens);
+
+    }
+
+}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/TokenInjectingConnectionGroup.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/TokenInjectingConnectionGroup.java
new file mode 100644
index 0000000..0ec93ba
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/TokenInjectingConnectionGroup.java
@@ -0,0 +1,69 @@
+/*
+ * 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.net.auth;
+
+import java.util.HashMap;
+import java.util.Map;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.GuacamoleTunnel;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
+
+/**
+ * ConnectionGroup implementation which overrides the connect() function of an
+ * underlying ConnectionGroup, adding a given set of parameter tokens to the
+ * tokens already supplied.
+ */
+public class TokenInjectingConnectionGroup extends DelegatingConnectionGroup {
+
+    /**
+     * The additional tokens to include with each call to connect().
+     */
+    private final Map<String, String> tokens;
+
+    /**
+     * Wraps the given ConnectionGroup, automatically adding the given tokens
+     * to each invocation of connect(). Any additional tokens which have the
+     * same name as existing tokens will override the existing values.
+     *
+     * @param connectionGroup
+     *     The ConnectionGroup to wrap.
+     *
+     * @param tokens
+     *     The additional tokens to include with each call to connect().
+     */
+    public TokenInjectingConnectionGroup(ConnectionGroup connectionGroup,
+            Map<String, String> tokens) {
+        super(connectionGroup);
+        this.tokens = tokens;
+    }
+
+    @Override
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+
+        // Apply provided tokens over those given to connect()
+        tokens = new HashMap<>(tokens);
+        tokens.putAll(this.tokens);
+
+        return super.connect(info, tokens);
+
+    }
+
+}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/TokenInjectingUserContext.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/TokenInjectingUserContext.java
new file mode 100644
index 0000000..a1ede96
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/TokenInjectingUserContext.java
@@ -0,0 +1,144 @@
+/*
+ * 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.net.auth;
+
+import java.util.Collections;
+import java.util.Map;
+import org.apache.guacamole.GuacamoleException;
+
+/**
+ * UserContext implementation which decorates a given UserContext,
+ * automatically applying additional parameter tokens during the connection
+ * process of any retrieved Connections and ConnectionGroups.
+ */
+public class TokenInjectingUserContext extends DelegatingUserContext {
+
+    /**
+     * The additional tokens to include with each call to connect() if
+     * getTokens() is not overridden.
+     */
+    private final Map<String, String> tokens;
+
+    /**
+     * Wraps the given UserContext, overriding the connect() function of each
+     * retrieved Connection and ConnectionGroup such that the given additional
+     * parameter tokens are included. Any additional tokens which have the same
+     * name as existing tokens will override the existing values. If tokens
+     * specific to a particular connection or connection group need to be
+     * included, getTokens() may be overridden to provide a different set of
+     * tokens.
+     *
+     * @param userContext
+     *     The UserContext to wrap.
+     *
+     * @param tokens
+     *     The additional tokens to include with each call to connect().
+     */
+    public TokenInjectingUserContext(UserContext userContext,
+            Map<String, String> tokens) {
+        super(userContext);
+        this.tokens = tokens;
+    }
+
+    /**
+     * Wraps the given UserContext, overriding the connect() function of each
+     * retrieved Connection and ConnectionGroup such that the additional
+     * parameter tokens returned by getTokens() are included. Any additional
+     * tokens which have the same name as existing tokens will override the
+     * existing values.
+     *
+     * @param userContext
+     *     The UserContext to wrap.
+     */
+    public TokenInjectingUserContext(UserContext userContext) {
+        this(userContext, Collections.<String, String>emptyMap());
+    }
+
+    /**
+     * Returns the tokens which should be added to an in-progress call to
+     * connect() for the given Connection. If not overridden, this function
+     * will return the tokens provided when this instance of
+     * TokenInjectingUserContext was created.
+     *
+     * @param connection
+     *     The Connection on which connect() has been called.
+     *
+     * @return
+     *     The tokens which should be added to the in-progress call to
+     *     connect().
+     */
+    protected Map<String, String> getTokens(Connection connection) {
+        return tokens;
+    }
+
+    /**
+     * Returns the tokens which should be added to an in-progress call to
+     * connect() for the given ConnectionGroup. If not overridden, this
+     * function will return the tokens provided when this instance of
+     * TokenInjectingUserContext was created.
+     *
+     * @param connectionGroup
+     *     The ConnectionGroup on which connect() has been called.
+     *
+     * @return
+     *     The tokens which should be added to the in-progress call to
+     *     connect().
+     */
+    protected Map<String, String> getTokens(ConnectionGroup connectionGroup) {
+        return tokens;
+    }
+
+    @Override
+    public Directory<ConnectionGroup> getConnectionGroupDirectory()
+            throws GuacamoleException {
+        return new DecoratingDirectory<ConnectionGroup>(super.getConnectionGroupDirectory()) {
+
+            @Override
+            protected ConnectionGroup decorate(ConnectionGroup object) throws GuacamoleException {
+                return new TokenInjectingConnectionGroup(object, getTokens(object));
+            }
+
+            @Override
+            protected ConnectionGroup undecorate(ConnectionGroup object) throws GuacamoleException {
+                return ((TokenInjectingConnectionGroup) object).getDelegateConnectionGroup();
+            }
+
+        };
+    }
+
+    @Override
+    public Directory<Connection> getConnectionDirectory()
+            throws GuacamoleException {
+        return new DecoratingDirectory<Connection>(super.getConnectionDirectory()) {
+
+            @Override
+            protected Connection decorate(Connection object) throws GuacamoleException {
+                return new TokenInjectingConnection(object, getTokens(object));
+            }
+
+            @Override
+            protected Connection undecorate(Connection object) throws GuacamoleException {
+                return ((TokenInjectingConnection) object).getDelegateConnection();
+            }
+
+        };
+    }
+
+}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleAuthenticationProvider.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleAuthenticationProvider.java
index 7b1e3e7..202181a 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleAuthenticationProvider.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleAuthenticationProvider.java
@@ -31,8 +31,6 @@
 import org.apache.guacamole.net.auth.Credentials;
 import org.apache.guacamole.net.auth.UserContext;
 import org.apache.guacamole.protocol.GuacamoleConfiguration;
-import org.apache.guacamole.token.StandardTokens;
-import org.apache.guacamole.token.TokenFilter;
 
 /**
  * Provides means of retrieving a set of named GuacamoleConfigurations for a
@@ -140,84 +138,13 @@
 
     }
 
-    /**
-     * Given an arbitrary credentials object, returns a Map containing all
-     * configurations authorized by those credentials, filtering those
-     * configurations using a TokenFilter and the standard credential tokens
-     * (like ${GUAC_USERNAME} and ${GUAC_PASSWORD}). The keys of this Map
-     * are Strings which uniquely identify each configuration.
-     *
-     * @param credentials
-     *     The credentials to use to retrieve authorized configurations.
-     *
-     * @return
-     *     A Map of all configurations authorized by the given credentials, or
-     *     null if the credentials given are not authorized.
-     *
-     * @throws GuacamoleException
-     *     If an error occurs while retrieving configurations.
-     */
-    private Map<String, GuacamoleConfiguration>
-            getFilteredAuthorizedConfigurations(Credentials credentials)
-            throws GuacamoleException {
-
-        // Get configurations
-        Map<String, GuacamoleConfiguration> configs =
-                getAuthorizedConfigurations(credentials);
-
-        // Return as unauthorized if not authorized to retrieve configs
-        if (configs == null)
-            return null;
-
-        // Build credential TokenFilter
-        TokenFilter tokenFilter = new TokenFilter();
-        StandardTokens.addStandardTokens(tokenFilter, credentials);
-
-        // Filter each configuration
-        for (GuacamoleConfiguration config : configs.values())
-            tokenFilter.filterValues(config.getParameters());
-
-        return configs;
-
-    }
-
-    /**
-     * Given a user who has already been authenticated, returns a Map
-     * containing all configurations for which that user is authorized,
-     * filtering those configurations using a TokenFilter and the standard
-     * credential tokens (like ${GUAC_USERNAME} and ${GUAC_PASSWORD}). The keys
-     * of this Map are Strings which uniquely identify each configuration.
-     *
-     * @param authenticatedUser
-     *     The user whose authorized configurations are to be retrieved.
-     *
-     * @return
-     *     A Map of all configurations authorized for use by the given user, or
-     *     null if the user is not authorized to use any configurations.
-     *
-     * @throws GuacamoleException
-     *     If an error occurs while retrieving configurations.
-     */
-    private Map<String, GuacamoleConfiguration>
-            getFilteredAuthorizedConfigurations(AuthenticatedUser authenticatedUser)
-            throws GuacamoleException {
-
-        // Pull cached configurations, if any
-        if (authenticatedUser instanceof SimpleAuthenticatedUser && authenticatedUser.getAuthenticationProvider() == this)
-            return ((SimpleAuthenticatedUser) authenticatedUser).getAuthorizedConfigurations();
-
-        // Otherwise, pull using credentials
-        return getFilteredAuthorizedConfigurations(authenticatedUser.getCredentials());
-
-    }
-
     @Override
     public AuthenticatedUser authenticateUser(final Credentials credentials)
             throws GuacamoleException {
 
         // Get configurations
         Map<String, GuacamoleConfiguration> configs =
-                getFilteredAuthorizedConfigurations(credentials);
+                getAuthorizedConfigurations(credentials);
 
         // Return as unauthorized if not authorized to retrieve configs
         if (configs == null)
@@ -233,14 +160,14 @@
 
         // Get configurations
         Map<String, GuacamoleConfiguration> configs =
-                getFilteredAuthorizedConfigurations(authenticatedUser);
+                getAuthorizedConfigurations(authenticatedUser.getCredentials());
 
         // Return as unauthorized if not authorized to retrieve configs
         if (configs == null)
             return null;
 
         // Return user context restricted to authorized configs
-        return new SimpleUserContext(this, authenticatedUser.getIdentifier(), configs);
+        return new SimpleUserContext(this, authenticatedUser.getIdentifier(), configs, true);
 
     }
 
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnection.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnection.java
index 85783a0..2934cbe 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnection.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnection.java
@@ -38,45 +38,147 @@
 import org.apache.guacamole.protocol.ConfiguredGuacamoleSocket;
 import org.apache.guacamole.protocol.GuacamoleClientInformation;
 import org.apache.guacamole.protocol.GuacamoleConfiguration;
+import org.apache.guacamole.token.TokenFilter;
 
 /**
- * An extremely basic Connection implementation.
+ * A Connection implementation which establishes the underlying connection to
+ * guacd using the configuration information provided in guacamole.properties.
+ * Parameter tokens provided to connect() are automatically applied if
+ * explicitly requested. Tracking of active connections and connection history
+ * is not provided.
  */
 public class SimpleConnection extends AbstractConnection {
 
     /**
      * Backing configuration, containing all sensitive information.
      */
-    private GuacamoleConfiguration config;
+    private GuacamoleConfiguration fullConfig;
 
     /**
-     * Creates a completely uninitialized SimpleConnection.
+     * Whether parameter tokens in the underlying GuacamoleConfiguration should
+     * be automatically applied upon connecting. If false, parameter tokens
+     * will not be interpreted at all.
+     */
+    private final boolean interpretTokens;
+
+    /**
+     * The tokens which should apply strictly to the next call to
+     * {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation)}.
+     * This storage is intended as a temporary bridge allowing the old version
+     * of connect() to be overridden while still resulting in the same behavior
+     * as older versions of SimpleConnection. <strong>This storage should be
+     * removed once support for the old, deprecated connect() is removed.</strong>
+     */
+    private final ThreadLocal<Map<String, String>> currentTokens =
+            new ThreadLocal<Map<String, String>>() {
+
+        @Override
+        protected Map<String, String> initialValue() {
+            return Collections.emptyMap();
+        }
+
+    };
+
+    /**
+     * Creates a completely uninitialized SimpleConnection. The name,
+     * identifier, and configuration of this SimpleConnection must eventually
+     * be set before the SimpleConnection may be used. Parameter tokens within
+     * the GuacamoleConfiguration eventually supplied with
+     * {@link #setConfiguration(org.apache.guacamole.protocol.GuacamoleConfiguration)}
+     * will not be interpreted.
      */
     public SimpleConnection() {
+        this(false);
+    }
+
+    /**
+     * Creates a completely uninitialized SimpleConnection. The name,
+     * identifier, and configuration of this SimpleConnection must eventually
+     * be set before the SimpleConnection may be used. Parameter tokens within
+     * the GuacamoleConfiguration eventually supplied with
+     * {@link #setConfiguration(org.apache.guacamole.protocol.GuacamoleConfiguration)}
+     * will not be interpreted unless explicitly requested.
+     *
+     * @param interpretTokens
+     *     Whether parameter tokens in the underlying GuacamoleConfiguration
+     *     should be automatically applied upon connecting. If false, parameter
+     *     tokens will not be interpreted at all.
+     */
+    public SimpleConnection(boolean interpretTokens) {
+        this.interpretTokens = interpretTokens;
     }
 
     /**
      * Creates a new SimpleConnection having the given identifier and
-     * GuacamoleConfiguration.
+     * GuacamoleConfiguration. Parameter tokens within the
+     * GuacamoleConfiguration will not be interpreted unless explicitly
+     * requested.
      *
-     * @param name The name to associate with this connection.
-     * @param identifier The identifier to associate with this connection.
-     * @param config The configuration describing how to connect to this
-     *               connection.
+     * @param name
+     *     The name to associate with this connection.
+     *
+     * @param identifier
+     *     The identifier to associate with this connection.
+     *
+     * @param config
+     *     The configuration describing how to connect to this connection.
      */
     public SimpleConnection(String name, String identifier,
             GuacamoleConfiguration config) {
-        
-        // Set name
-        setName(name);
+        this(name, identifier, config, false);
+    }
 
-        // Set identifier
-        setIdentifier(identifier);
+    /**
+     * Creates a new SimpleConnection having the given identifier and
+     * GuacamoleConfiguration. Parameter tokens will be interpreted if
+     * explicitly requested.
+     *
+     * @param name
+     *     The name to associate with this connection.
+     *
+     * @param identifier
+     *     The identifier to associate with this connection.
+     *
+     * @param config
+     *     The configuration describing how to connect to this connection.
+     *
+     * @param interpretTokens
+     *     Whether parameter tokens in the underlying GuacamoleConfiguration
+     *     should be automatically applied upon connecting. If false, parameter
+     *     tokens will not be interpreted at all.
+     */
+    public SimpleConnection(String name, String identifier,
+            GuacamoleConfiguration config, boolean interpretTokens) {
 
-        // Set config
-        setConfiguration(config);
-        this.config = config;
+        super.setName(name);
+        super.setIdentifier(identifier);
+        super.setConfiguration(config);
 
+        this.fullConfig = config;
+        this.interpretTokens = interpretTokens;
+
+    }
+
+    /**
+     * Returns the GuacamoleConfiguration describing how to connect to this
+     * connection. Unlike {@link #getConfiguration()}, which is allowed to omit
+     * or tokenize information, the GuacamoleConfiguration returned by this
+     * function will always be the full configuration to be used to establish
+     * the connection, as provided when this SimpleConnection was created or via
+     * {@link #setConfiguration(org.apache.guacamole.protocol.GuacamoleConfiguration)}.
+     *
+     * @return
+     *     The full GuacamoleConfiguration describing how to connect to this
+     *     connection, without any information omitted or tokenized.
+     */
+    protected GuacamoleConfiguration getFullConfiguration() {
+        return fullConfig;
+    }
+
+    @Override
+    public void setConfiguration(GuacamoleConfiguration config) {
+        super.setConfiguration(config);
+        this.fullConfig = config;
     }
 
     @Override
@@ -95,6 +197,7 @@
     }
 
     @Override
+    @Deprecated
     public GuacamoleTunnel connect(GuacamoleClientInformation info)
             throws GuacamoleException {
 
@@ -106,6 +209,10 @@
         String hostname = proxyConfig.getHostname();
         int port = proxyConfig.getPort();
 
+        // Apply tokens to config parameters
+        GuacamoleConfiguration filteredConfig = new GuacamoleConfiguration(getFullConfiguration());
+        new TokenFilter(currentTokens.get()).filterValues(filteredConfig.getParameters());
+
         GuacamoleSocket socket;
 
         // Determine socket type based on required encryption method
@@ -115,7 +222,7 @@
             case SSL:
                 socket = new ConfiguredGuacamoleSocket(
                     new SSLGuacamoleSocket(hostname, port),
-                    config, info
+                    filteredConfig, info
                 );
                 break;
 
@@ -123,7 +230,7 @@
             case NONE:
                 socket = new ConfiguredGuacamoleSocket(
                     new InetGuacamoleSocket(hostname, port),
-                    config, info
+                    filteredConfig, info
                 );
                 break;
 
@@ -134,6 +241,41 @@
         }
 
         return new SimpleGuacamoleTunnel(socket);
+
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This implementation will connect using the GuacamoleConfiguration
+     * returned by {@link #getFullConfiguration()}, honoring the
+     * "guacd-hostname", "guacd-port", and "guacd-ssl" properties set within
+     * guacamole.properties. Parameter tokens will be taken into account if
+     * the SimpleConnection was explicitly requested to do so when created.
+     *
+     * <p>Implementations requiring more complex behavior should consider using
+     * the {@link AbstractConnection} base class or implementing
+     * {@link org.apache.guacamole.net.auth.Connection} directly.
+     */
+    @Override
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+
+        // Make received tokens available within the legacy connect() strictly
+        // in context of the current connect() call
+        try {
+
+            // Automatically filter configurations only if explicitly
+            // configured to do so
+            if (interpretTokens)
+                currentTokens.set(tokens);
+
+            return connect(info);
+
+        }
+        finally {
+            currentTokens.remove();
+        }
         
     }
 
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnectionGroup.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnectionGroup.java
index 3a7df28..b2f7de0 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnectionGroup.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleConnectionGroup.java
@@ -47,7 +47,7 @@
      * The identifiers of all connection groups in this group.
      */
     private final Set<String> connectionGroupIdentifiers;
-    
+
     /**
      * Creates a new SimpleConnectionGroup having the given name and identifier
      * which will expose the given contents.
@@ -109,9 +109,16 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info) 
+    @Deprecated
+    public GuacamoleTunnel connect(GuacamoleClientInformation info)
             throws GuacamoleException {
         throw new GuacamoleSecurityException("Permission denied.");
     }
 
+    @Override
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+        return connect(info);
+    }
+
 }
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleUserContext.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleUserContext.java
index 03e94fb..2cefee2 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleUserContext.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/auth/simple/SimpleUserContext.java
@@ -59,7 +59,8 @@
      * Creates a new SimpleUserContext which provides access to only those
      * configurations within the given Map. The username is set to the
      * ANONYMOUS_IDENTIFIER defined by AuthenticatedUser, effectively declaring
-     * the current user as anonymous.
+     * the current user as anonymous. Parameter tokens within the given
+     * GuacamoleConfigurations will not be interpreted.
      *
      * @param authProvider
      *     The AuthenticationProvider creating this UserContext.
@@ -76,6 +77,8 @@
     /**
      * Creates a new SimpleUserContext for the user with the given username
      * which provides access to only those configurations within the given Map.
+     * Parameter tokens within the given GuacamoleConfigurations will not be
+     * interpreted.
      *
      * @param authProvider
      *     The AuthenticationProvider creating this UserContext.
@@ -89,6 +92,33 @@
      */
     public SimpleUserContext(AuthenticationProvider authProvider,
             String username, Map<String, GuacamoleConfiguration> configs) {
+        this(authProvider, username, configs, false);
+    }
+
+    /**
+     * Creates a new SimpleUserContext for the user with the given username
+     * which provides access to only those configurations within the given Map.
+     * Parameter tokens within the given GuacamoleConfigurations will be
+     * interpreted if explicitly requested.
+     *
+     * @param authProvider
+     *     The AuthenticationProvider creating this UserContext.
+     *
+     * @param username
+     *     The username of the user associated with this UserContext.
+     *
+     * @param configs
+     *     A Map of all configurations for which the user associated with
+     *     this UserContext has read access.
+     *
+     * @param interpretTokens
+     *     Whether parameter tokens in the underlying GuacamoleConfigurations
+     *     should be automatically applied upon connecting. If false, parameter
+     *     tokens will not be interpreted at all.
+     */
+    public SimpleUserContext(AuthenticationProvider authProvider,
+            String username, Map<String, GuacamoleConfiguration> configs,
+            boolean interpretTokens) {
 
         // Produce map of connections from given configs
         Map<String, Connection> connections = new ConcurrentHashMap<String, Connection>(configs.size());
@@ -99,7 +129,7 @@
             GuacamoleConfiguration config = configEntry.getValue();
 
             // Add as simple connection
-            Connection connection = new SimpleConnection(identifier, identifier, config);
+            Connection connection = new SimpleConnection(identifier, identifier, config, interpretTokens);
             connection.setParentIdentifier(DEFAULT_ROOT_CONNECTION_GROUP);
             connections.put(identifier, connection);
 
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/properties/URIGuacamoleProperty.java b/guacamole-ext/src/main/java/org/apache/guacamole/properties/URIGuacamoleProperty.java
new file mode 100644
index 0000000..d40f91e
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/properties/URIGuacamoleProperty.java
@@ -0,0 +1,45 @@
+/*
+ * 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.properties;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
+
+/**
+ * A GuacamoleProperty whose value is a URI.
+ */
+public abstract class URIGuacamoleProperty implements GuacamoleProperty<URI> {
+    
+    @Override
+    public URI parseValue(String value) throws GuacamoleException {
+        
+        try {
+            return new URI(value);
+        }
+        catch (URISyntaxException e) {
+            throw new GuacamoleServerException("Value \"" + value
+                + "\" is not a valid URI.");
+        }
+        
+    }
+    
+}
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/token/StandardTokens.java b/guacamole-ext/src/main/java/org/apache/guacamole/token/StandardTokens.java
index b1b280b..3118cf2 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/token/StandardTokens.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/token/StandardTokens.java
@@ -27,7 +27,12 @@
 /**
  * Utility class which provides access to standardized token names, as well as
  * facilities for generating those tokens from common objects.
+ *
+ * @deprecated Standard tokens are now supplied by default to the connect()
+ * functions of connections and connection groups. Manually generating the
+ * standard tokens is not necessary.
  */
+@Deprecated
 public class StandardTokens {
 
     /**
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java b/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java
index ab43622..c412d18 100644
--- a/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenFilter.java
@@ -19,6 +19,7 @@
 
 package org.apache.guacamole.token;
 
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.regex.Matcher;
@@ -68,7 +69,27 @@
     /**
      * The values of all known tokens.
      */
-    private final Map<String, String> tokenValues = new HashMap<String, String>();
+    private final Map<String, String> tokenValues;
+
+    /**
+     * Creates a new TokenFilter which has no associated tokens. Tokens must
+     * later be given using {@link #setToken(java.lang.String, java.lang.String)}
+     * or {@link #setTokens(java.util.Map)}.
+     */
+    public TokenFilter() {
+         this(Collections.<String, String>emptyMap());
+    }
+
+    /**
+     * Creates a new TokenFilter which is initialized with the given token
+     * name/value pairs.
+     *
+     * @param tokenValues
+     *     A map containing token names and their corresponding values.
+     */
+    public TokenFilter(Map<String, String> tokenValues) {
+        this.tokenValues = new HashMap<>(tokenValues);
+    }
 
     /**
      * Sets the token having the given name to the given value. Any existing
diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenName.java b/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenName.java
new file mode 100644
index 0000000..ae83346
--- /dev/null
+++ b/guacamole-ext/src/main/java/org/apache/guacamole/token/TokenName.java
@@ -0,0 +1,122 @@
+/*
+ * 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 java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class for generating parameter token names.
+ */
+public class TokenName {
+
+    /**
+     * 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 STRING_NAME_GROUPING = Pattern.compile(
+
+        // "Camel" word groups
+        "\\p{javaUpperCase}\\p{javaLowerCase}+"
+
+        // Groups of digits
+        + "|[0-9]+"
+
+        // Groups of uppercase letters, excluding the uppercase letter
+        // which begins a following "Camel" group
+        + "|\\p{javaUpperCase}+(?!\\p{javaLowerCase})"
+
+        // Groups of lowercase letters which match no other pattern
+        + "|\\p{javaLowerCase}+"
+
+        // Groups of word characters letters which match no other pattern
+        + "|\\b\\w+\\b"
+
+    );
+
+    /**
+     * This utility class should not be instantiated.
+     */
+    private TokenName() {}
+
+    /**
+     * Generates the name of the parameter token that should be populated with
+     * 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 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
+     *     given string.
+     */
+    public static String canonicalize(final String name, final String prefix) {
+
+        // If even one logical word grouping cannot be found, default to
+        // simply converting the string to uppercase and adding the
+        // prefix
+        Matcher groupMatcher = STRING_NAME_GROUPING.matcher(name);
+        if (!groupMatcher.find())
+            return prefix + name.toUpperCase();
+
+        // Split the given name into logical word groups, separated by
+        // underscores and converted to uppercase
+        StringBuilder builder = new StringBuilder(prefix);
+        builder.append(groupMatcher.group(0).toUpperCase());
+
+        while (groupMatcher.find()) {
+            builder.append("_");
+            builder.append(groupMatcher.group(0).toUpperCase());
+        }
+
+        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 0ee95ae..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,14 +99,20 @@
                         "failsafe",
                         "fr-fr-azerty",
                         "fr-ch-qwertz",
+                        "hu-hu-qwertz",                        
                         "it-it-qwerty",
                         "ja-jp-qwerty",
                         "pt-br-qwerty",
                         "sv-se-qwerty",
+                        "da-dk-qwerty",
                         "tr-tr-qwerty"
                     ]
                 },
                 {
+                    "name"  : "timezone",
+                    "type"  : "TIMEZONE"
+                },
+                {
                     "name"    : "console",
                     "type"    : "BOOLEAN",
                     "options" : [ "true" ]
@@ -147,6 +154,22 @@
         },
 
         {
+            "name"  : "clipboard",
+            "fields" : [
+                {
+                    "name"    : "disable-copy",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
+                },
+                {
+                    "name"    : "disable-paste",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
+                }
+            ]
+        },
+
+        {
             "name"  : "device-redirection",
             "fields" : [
                 {
diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/ssh.json
index a71e1fb..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" ]
@@ -68,6 +72,22 @@
         },
 
         {
+            "name"  : "clipboard",
+            "fields" : [
+                {
+                    "name"    : "disable-copy",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
+                },
+                {
+                    "name"    : "disable-paste",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
+                }
+            ]
+        },
+
+        {
             "name" : "session",
             "fields" : [
                 {
@@ -75,11 +95,20 @@
                     "type"  : "TEXT"
                 },
                 {
+                    "name"  : "locale",
+                    "type"  : "TEXT"
+                },
+                {
+                    "name"  : "timezone",
+                    "type"  : "TIMEZONE"
+                },
+                {
                     "name"  : "server-alive-interval",
                     "type"  : "NUMERIC"
                 }
             ]
         },
+
         {
             "name" : "behavior",
             "fields" : [
@@ -89,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 60064a7..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
@@ -28,8 +28,20 @@
                     "type"  : "PASSWORD"
                 },
                 {
+                    "name"  : "username-regex",
+                    "type"  : "TEXT"
+                },
+                {
                     "name"  : "password-regex",
                     "type"  : "TEXT"
+                },
+                {
+                    "name"  : "login-success-regex",
+                    "type"  : "TEXT"
+                },
+                {
+                    "name"  : "login-failure-regex",
+                    "type"  : "TEXT"
                 }
             ]
         },
@@ -39,7 +51,7 @@
             "fields" : [
                 {
                     "name"  : "color-scheme",
-                    "type"  : "TEXT",
+                    "type"  : "TERMINAL_COLOR_SCHEME",
                     "options" : [ "", "black-white", "gray-black", "green-black", "white-black" ]
                 },
                 {
@@ -52,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" ]
@@ -60,6 +76,22 @@
         },
 
         {
+            "name"  : "clipboard",
+            "fields" : [
+                {
+                    "name"    : "disable-copy",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
+                },
+                {
+                    "name"    : "disable-paste",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
+                }
+            ]
+        },
+
+        {
             "name" : "behavior",
             "fields" : [
                 {
diff --git a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/vnc.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/vnc.json
index bdeaebc..cc0a75a 100644
--- a/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/vnc.json
+++ b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/vnc.json
@@ -59,6 +59,16 @@
                     "name"    : "clipboard-encoding",
                     "type"    : "ENUM",
                     "options" : [ "", "ISO8859-1", "UTF-8", "UTF-16", "CP1252" ]
+                },
+                {
+                    "name"    : "disable-copy",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
+                },
+                {
+                    "name"    : "disable-paste",
+                    "type"    : "BOOLEAN",
+                    "options" : [ "true" ]
                 }
             ]
         },
diff --git a/guacamole-ext/src/test/java/org/apache/guacamole/token/TokenNameTest.java b/guacamole-ext/src/test/java/org/apache/guacamole/token/TokenNameTest.java
new file mode 100644
index 0000000..a1d3e2f
--- /dev/null
+++ b/guacamole-ext/src/test/java/org/apache/guacamole/token/TokenNameTest.java
@@ -0,0 +1,55 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.guacamole.token;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+/**
+ * Test which verifies automatic generation of connection parameter token names.
+ */
+public class TokenNameTest {
+
+    /**
+     * Verifies that TokenName.canonicalize() generates token names as
+     * specified, regardless of the format of the provided string.
+     */
+    @Test
+    public void testCanonicalize() {
+        assertEquals("A", TokenName.canonicalize("a"));
+        assertEquals("B", TokenName.canonicalize("b"));
+        assertEquals("1", TokenName.canonicalize("1"));
+        assertEquals("SOME_URL", TokenName.canonicalize("someURL"));
+        assertEquals("LOWERCASE_WITH_DASHES", TokenName.canonicalize("lowercase-with-dashes"));
+        assertEquals("HEADLESS_CAMEL_CASE", TokenName.canonicalize("headlessCamelCase"));
+        assertEquals("CAMEL_CASE", TokenName.canonicalize("CamelCase"));
+        assertEquals("CAMEL_CASE", TokenName.canonicalize("CamelCase"));
+        assertEquals("LOWERCASE_WITH_UNDERSCORES", TokenName.canonicalize("lowercase_with_underscores"));
+        assertEquals("UPPERCASE_WITH_UNDERSCORES", TokenName.canonicalize("UPPERCASE_WITH_UNDERSCORES"));
+        assertEquals("A_VERY_INCONSISTENT_MIX_OF_ALL_STYLES", TokenName.canonicalize("aVery-INCONSISTENTMix_ofAllStyles"));
+        assertEquals("ABC_123_DEF_456", TokenName.canonicalize("abc123def456"));
+        assertEquals("ABC_123_DEF_456", TokenName.canonicalize("ABC123DEF456"));
+        assertEquals("WORD_A_WORD_AB_WORD_ABC_WORD", TokenName.canonicalize("WordAWordABWordABCWord"));
+        
+        assertEquals("AUTH_ATTRIBUTE", TokenName.canonicalize("Attribute", "AUTH_"));
+        assertEquals("auth_SOMETHING", TokenName.canonicalize("Something", "auth_"));
+    }
+
+}
diff --git a/guacamole/pom.xml b/guacamole/pom.xml
index e1a2b47..d7cf465 100644
--- a/guacamole/pom.xml
+++ b/guacamole/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole</artifactId>
     <packaging>war</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole</name>
     <url>http://guacamole.apache.org/</url>
 
@@ -257,28 +257,28 @@
         <dependency>
             <groupId>ch.qos.logback</groupId>
             <artifactId>logback-classic</artifactId>
-            <version>1.1.2</version>
+            <version>1.2.3</version>
         </dependency>
         
         <!-- Guacamole Java API -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
         <!-- Guacamole Extension API -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-ext</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
         </dependency>
 
         <!-- Guacamole JavaScript API -->
         <dependency>
             <groupId>org.apache.guacamole</groupId>
             <artifactId>guacamole-common-js</artifactId>
-            <version>1.0.0</version>
+            <version>1.1.0</version>
             <type>zip</type>
             <scope>runtime</scope>
         </dependency>
@@ -493,6 +493,27 @@
             </exclusions>
 
         </dependency>
+        
+        <!-- Guava Base Libraries -->
+        <dependency>
+            <groupId>com.google.guava</groupId>
+            <artifactId>guava</artifactId>
+            <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>
 
diff --git a/guacamole/src/licenses/LICENSE b/guacamole/src/licenses/LICENSE
index 51f5b21..2995811 100644
--- a/guacamole/src/licenses/LICENSE
+++ b/guacamole/src/licenses/LICENSE
@@ -529,6 +529,15 @@
 the terms above.
 
 
+Guava: Google Core Libraries for Java (https://github.com/google/guava)
+-----------------------------------------------------------------------
+
+    Version: 27.0.1-jre
+    From: 'Google Inc.' (http://www.google.com/)
+    License(s):
+        Apache v2.0 (bundled/guava-27.0.1-jre/COPYING)
+
+
 JSR-250 Reference Implementation
 (https://jcp.org/aboutJava/communityprocess/final/jsr250/index.html)
 --------------------------------------------------------------------
@@ -605,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/)
 --------------------------------
 
@@ -658,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/NOTICE b/guacamole/src/licenses/NOTICE
index 97e6130..39de3ec 100644
--- a/guacamole/src/licenses/NOTICE
+++ b/guacamole/src/licenses/NOTICE
@@ -1,5 +1,5 @@
 Apache Guacamole
-Copyright 2018 The Apache Software Foundation
+Copyright 2019 The Apache Software Foundation
 
 This product includes software developed at
 The Apache Software Foundation (http://www.apache.org/).
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/bundled/guava-18.0/COPYING b/guacamole/src/licenses/bundled/guava-27.0.1-jre/COPYING
similarity index 100%
copy from extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-dist/src/licenses/bundled/guava-18.0/COPYING
copy to guacamole/src/licenses/bundled/guava-27.0.1-jre/COPYING
diff --git a/guacamole/src/licenses/bundled/jaxb-api-2.2.2/License.txt b/guacamole/src/licenses/bundled/jaxb-api-2.2.2/License.txt
deleted file mode 100644
index 159c16b..0000000
--- a/guacamole/src/licenses/bundled/jaxb-api-2.2.2/License.txt
+++ /dev/null
@@ -1,136 +0,0 @@
-COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)Version 1.1
-
-1. Definitions.
-
-     1.1. "Contributor" means each individual or entity that creates or contributes to the creation of Modifications.
-
-     1.2. "Contributor Version" means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor.
-
-     1.3. "Covered Software" means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof.
-
-     1.4. "Executable" means the Covered Software in any form other than Source Code.
-
-     1.5. "Initial Developer" means the individual or entity that first makes Original Software available under this License.
-
-     1.6. "Larger Work" means a work which combines Covered Software or portions thereof with code not governed by the terms of this License.
-
-     1.7. "License" means this document.
-
-     1.8. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein.
-
-     1.9. "Modifications" means the Source Code and Executable form of any of the following:
-
-     A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications;
-
-     B. Any new file that contains any part of the Original Software or previous Modification; or
-
-     C. Any new file that is contributed or otherwise made available under the terms of this License.
-
-     1.10. "Original Software" means the Source Code and Executable form of computer software code that is originally released under this License.
-
-     1.11. "Patent Claims" means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor.
-
-     1.12. "Source Code" means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code.
-
-     1.13. "You" (or "Your") means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.
-
-2. License Grants.
-
-     2.1. The Initial Developer Grant.
-
-     Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license:
-
-     (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and
-
-     (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof).
-
-     (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License.
-
-     (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices.
-
-     2.2. Contributor Grant.
-
-     Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:
-
-     (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and
-
-     (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination).
-
-     (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party.
-
-     (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor.
-
-3. Distribution Obligations.
-
-     3.1. Availability of Source Code.
-
-     Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange.
-
-     3.2. Modifications.
-
-     The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License.
-
-     3.3. Required Notices.
-
-     You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer.
-
-     3.4. Application of Additional Terms.
-
-     You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients' rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer.
-
-     3.5. Distribution of Executable Versions.
-
-     You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient's rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer.
-
-     3.6. Larger Works.
-
-     You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software.
-
-4. Versions of the License.
-
-     4.1. New Versions.
-
-     Oracle is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License.
-
-     4.2. Effect of New Versions.
-
-     You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward.
-
-     4.3. Modified Versions.
-
-     When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License.
-
-5. DISCLAIMER OF WARRANTY.
-
-     COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
-
-6. TERMINATION.
-
-     6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive.
-
-     6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as "Participant") alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant.
-
-     6.3. If You assert a patent infringement claim against Participant alleging that the Participant Software directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by such Participant under Sections 2.1 or 2.2 shall be taken into account in determining the amount or value of any payment or license.
-
-     6.4. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination.
-
-7. LIMITATION OF LIABILITY.
-
-     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
-
-8. U.S. GOVERNMENT END USERS.
-
-     The Covered Software is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer software" (as that term is defined at 48 C.F.R. ? 252.227-7014(a)(1)) and "commercial computer software documentation" as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License.
-
-9. MISCELLANEOUS.
-
-     This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction's conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys' fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software.
-
-10. RESPONSIBILITY FOR CLAIMS.
-
-     As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.
-
-----------
-NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)
-The code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California.
-
diff --git a/guacamole/src/licenses/bundled/jaxb-impl-2.2.3-1/License.txt b/guacamole/src/licenses/bundled/jaxb-impl-2.2.3-1/License.txt
deleted file mode 100644
index 159c16b..0000000
--- a/guacamole/src/licenses/bundled/jaxb-impl-2.2.3-1/License.txt
+++ /dev/null
@@ -1,136 +0,0 @@
-COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)Version 1.1
-
-1. Definitions.
-
-     1.1. "Contributor" means each individual or entity that creates or contributes to the creation of Modifications.
-
-     1.2. "Contributor Version" means the combination of the Original Software, prior Modifications used by a Contributor (if any), and the Modifications made by that particular Contributor.
-
-     1.3. "Covered Software" means (a) the Original Software, or (b) Modifications, or (c) the combination of files containing Original Software with files containing Modifications, in each case including portions thereof.
-
-     1.4. "Executable" means the Covered Software in any form other than Source Code.
-
-     1.5. "Initial Developer" means the individual or entity that first makes Original Software available under this License.
-
-     1.6. "Larger Work" means a work which combines Covered Software or portions thereof with code not governed by the terms of this License.
-
-     1.7. "License" means this document.
-
-     1.8. "Licensable" means having the right to grant, to the maximum extent possible, whether at the time of the initial grant or subsequently acquired, any and all of the rights conveyed herein.
-
-     1.9. "Modifications" means the Source Code and Executable form of any of the following:
-
-     A. Any file that results from an addition to, deletion from or modification of the contents of a file containing Original Software or previous Modifications;
-
-     B. Any new file that contains any part of the Original Software or previous Modification; or
-
-     C. Any new file that is contributed or otherwise made available under the terms of this License.
-
-     1.10. "Original Software" means the Source Code and Executable form of computer software code that is originally released under this License.
-
-     1.11. "Patent Claims" means any patent claim(s), now owned or hereafter acquired, including without limitation, method, process, and apparatus claims, in any patent Licensable by grantor.
-
-     1.12. "Source Code" means (a) the common form of computer software code in which modifications are made and (b) associated documentation included in or with such code.
-
-     1.13. "You" (or "Your") means an individual or a legal entity exercising rights under, and complying with all of the terms of, this License. For legal entities, "You" includes any entity which controls, is controlled by, or is under common control with You. For purposes of this definition, "control" means (a) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (b) ownership of more than fifty percent (50%) of the outstanding shares or beneficial ownership of such entity.
-
-2. License Grants.
-
-     2.1. The Initial Developer Grant.
-
-     Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, the Initial Developer hereby grants You a world-wide, royalty-free, non-exclusive license:
-
-     (a) under intellectual property rights (other than patent or trademark) Licensable by Initial Developer, to use, reproduce, modify, display, perform, sublicense and distribute the Original Software (or portions thereof), with or without Modifications, and/or as part of a Larger Work; and
-
-     (b) under Patent Claims infringed by the making, using or selling of Original Software, to make, have made, use, practice, sell, and offer for sale, and/or otherwise dispose of the Original Software (or portions thereof).
-
-     (c) The licenses granted in Sections 2.1(a) and (b) are effective on the date Initial Developer first distributes or otherwise makes the Original Software available to a third party under the terms of this License.
-
-     (d) Notwithstanding Section 2.1(b) above, no patent license is granted: (1) for code that You delete from the Original Software, or (2) for infringements caused by: (i) the modification of the Original Software, or (ii) the combination of the Original Software with other software or devices.
-
-     2.2. Contributor Grant.
-
-     Conditioned upon Your compliance with Section 3.1 below and subject to third party intellectual property claims, each Contributor hereby grants You a world-wide, royalty-free, non-exclusive license:
-
-     (a) under intellectual property rights (other than patent or trademark) Licensable by Contributor to use, reproduce, modify, display, perform, sublicense and distribute the Modifications created by such Contributor (or portions thereof), either on an unmodified basis, with other Modifications, as Covered Software and/or as part of a Larger Work; and
-
-     (b) under Patent Claims infringed by the making, using, or selling of Modifications made by that Contributor either alone and/or in combination with its Contributor Version (or portions of such combination), to make, use, sell, offer for sale, have made, and/or otherwise dispose of: (1) Modifications made by that Contributor (or portions thereof); and (2) the combination of Modifications made by that Contributor with its Contributor Version (or portions of such combination).
-
-     (c) The licenses granted in Sections 2.2(a) and 2.2(b) are effective on the date Contributor first distributes or otherwise makes the Modifications available to a third party.
-
-     (d) Notwithstanding Section 2.2(b) above, no patent license is granted: (1) for any code that Contributor has deleted from the Contributor Version; (2) for infringements caused by: (i) third party modifications of Contributor Version, or (ii) the combination of Modifications made by that Contributor with other software (except as part of the Contributor Version) or other devices; or (3) under Patent Claims infringed by Covered Software in the absence of Modifications made by that Contributor.
-
-3. Distribution Obligations.
-
-     3.1. Availability of Source Code.
-
-     Any Covered Software that You distribute or otherwise make available in Executable form must also be made available in Source Code form and that Source Code form must be distributed only under the terms of this License. You must include a copy of this License with every copy of the Source Code form of the Covered Software You distribute or otherwise make available. You must inform recipients of any such Covered Software in Executable form as to how they can obtain such Covered Software in Source Code form in a reasonable manner on or through a medium customarily used for software exchange.
-
-     3.2. Modifications.
-
-     The Modifications that You create or to which You contribute are governed by the terms of this License. You represent that You believe Your Modifications are Your original creation(s) and/or You have sufficient rights to grant the rights conveyed by this License.
-
-     3.3. Required Notices.
-
-     You must include a notice in each of Your Modifications that identifies You as the Contributor of the Modification. You may not remove or alter any copyright, patent or trademark notices contained within the Covered Software, or any notices of licensing or any descriptive text giving attribution to any Contributor or the Initial Developer.
-
-     3.4. Application of Additional Terms.
-
-     You may not offer or impose any terms on any Covered Software in Source Code form that alters or restricts the applicable version of this License or the recipients' rights hereunder. You may choose to offer, and to charge a fee for, warranty, support, indemnity or liability obligations to one or more recipients of Covered Software. However, you may do so only on Your own behalf, and not on behalf of the Initial Developer or any Contributor. You must make it absolutely clear that any such warranty, support, indemnity or liability obligation is offered by You alone, and You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of warranty, support, indemnity or liability terms You offer.
-
-     3.5. Distribution of Executable Versions.
-
-     You may distribute the Executable form of the Covered Software under the terms of this License or under the terms of a license of Your choice, which may contain terms different from this License, provided that You are in compliance with the terms of this License and that the license for the Executable form does not attempt to limit or alter the recipient's rights in the Source Code form from the rights set forth in this License. If You distribute the Covered Software in Executable form under a different license, You must make it absolutely clear that any terms which differ from this License are offered by You alone, not by the Initial Developer or Contributor. You hereby agree to indemnify the Initial Developer and every Contributor for any liability incurred by the Initial Developer or such Contributor as a result of any such terms You offer.
-
-     3.6. Larger Works.
-
-     You may create a Larger Work by combining Covered Software with other code not governed by the terms of this License and distribute the Larger Work as a single product. In such a case, You must make sure the requirements of this License are fulfilled for the Covered Software.
-
-4. Versions of the License.
-
-     4.1. New Versions.
-
-     Oracle is the initial license steward and may publish revised and/or new versions of this License from time to time. Each version will be given a distinguishing version number. Except as provided in Section 4.3, no one other than the license steward has the right to modify this License.
-
-     4.2. Effect of New Versions.
-
-     You may always continue to use, distribute or otherwise make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. If the Initial Developer includes a notice in the Original Software prohibiting it from being distributed or otherwise made available under any subsequent version of the License, You must distribute and make the Covered Software available under the terms of the version of the License under which You originally received the Covered Software. Otherwise, You may also choose to use, distribute or otherwise make the Covered Software available under the terms of any subsequent version of the License published by the license steward.
-
-     4.3. Modified Versions.
-
-     When You are an Initial Developer and You want to create a new license for Your Original Software, You may create and use a modified version of this License if You: (a) rename the license and remove any references to the name of the license steward (except to note that the license differs from this License); and (b) otherwise make it clear that the license contains terms which differ from this License.
-
-5. DISCLAIMER OF WARRANTY.
-
-     COVERED SOFTWARE IS PROVIDED UNDER THIS LICENSE ON AN "AS IS" BASIS, WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, WITHOUT LIMITATION, WARRANTIES THAT THE COVERED SOFTWARE IS FREE OF DEFECTS, MERCHANTABLE, FIT FOR A PARTICULAR PURPOSE OR NON-INFRINGING. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE COVERED SOFTWARE IS WITH YOU. SHOULD ANY COVERED SOFTWARE PROVE DEFECTIVE IN ANY RESPECT, YOU (NOT THE INITIAL DEVELOPER OR ANY OTHER CONTRIBUTOR) ASSUME THE COST OF ANY NECESSARY SERVICING, REPAIR OR CORRECTION. THIS DISCLAIMER OF WARRANTY CONSTITUTES AN ESSENTIAL PART OF THIS LICENSE. NO USE OF ANY COVERED SOFTWARE IS AUTHORIZED HEREUNDER EXCEPT UNDER THIS DISCLAIMER.
-
-6. TERMINATION.
-
-     6.1. This License and the rights granted hereunder will terminate automatically if You fail to comply with terms herein and fail to cure such breach within 30 days of becoming aware of the breach. Provisions which, by their nature, must remain in effect beyond the termination of this License shall survive.
-
-     6.2. If You assert a patent infringement claim (excluding declaratory judgment actions) against Initial Developer or a Contributor (the Initial Developer or Contributor against whom You assert such claim is referred to as "Participant") alleging that the Participant Software (meaning the Contributor Version where the Participant is a Contributor or the Original Software where the Participant is the Initial Developer) directly or indirectly infringes any patent, then any and all rights granted directly or indirectly to You by such Participant, the Initial Developer (if the Initial Developer is not the Participant) and all Contributors under Sections 2.1 and/or 2.2 of this License shall, upon 60 days notice from Participant terminate prospectively and automatically at the expiration of such 60 day notice period, unless if within such 60 day period You withdraw Your claim with respect to the Participant Software against such Participant either unilaterally or pursuant to a written agreement with Participant.
-
-     6.3. If You assert a patent infringement claim against Participant alleging that the Participant Software directly or indirectly infringes any patent where such claim is resolved (such as by license or settlement) prior to the initiation of patent infringement litigation, then the reasonable value of the licenses granted by such Participant under Sections 2.1 or 2.2 shall be taken into account in determining the amount or value of any payment or license.
-
-     6.4. In the event of termination under Sections 6.1 or 6.2 above, all end user licenses that have been validly granted by You or any distributor hereunder prior to termination (excluding licenses granted to You by any distributor) shall survive termination.
-
-7. LIMITATION OF LIABILITY.
-
-     UNDER NO CIRCUMSTANCES AND UNDER NO LEGAL THEORY, WHETHER TORT (INCLUDING NEGLIGENCE), CONTRACT, OR OTHERWISE, SHALL YOU, THE INITIAL DEVELOPER, ANY OTHER CONTRIBUTOR, OR ANY DISTRIBUTOR OF COVERED SOFTWARE, OR ANY SUPPLIER OF ANY OF SUCH PARTIES, BE LIABLE TO ANY PERSON FOR ANY INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES OF ANY CHARACTER INCLUDING, WITHOUT LIMITATION, DAMAGES FOR LOSS OF GOODWILL, WORK STOPPAGE, COMPUTER FAILURE OR MALFUNCTION, OR ANY AND ALL OTHER COMMERCIAL DAMAGES OR LOSSES, EVEN IF SUCH PARTY SHALL HAVE BEEN INFORMED OF THE POSSIBILITY OF SUCH DAMAGES. THIS LIMITATION OF LIABILITY SHALL NOT APPLY TO LIABILITY FOR DEATH OR PERSONAL INJURY RESULTING FROM SUCH PARTY'S NEGLIGENCE TO THE EXTENT APPLICABLE LAW PROHIBITS SUCH LIMITATION. SOME JURISDICTIONS DO NOT ALLOW THE EXCLUSION OR LIMITATION OF INCIDENTAL OR CONSEQUENTIAL DAMAGES, SO THIS EXCLUSION AND LIMITATION MAY NOT APPLY TO YOU.
-
-8. U.S. GOVERNMENT END USERS.
-
-     The Covered Software is a "commercial item," as that term is defined in 48 C.F.R. 2.101 (Oct. 1995), consisting of "commercial computer software" (as that term is defined at 48 C.F.R. ? 252.227-7014(a)(1)) and "commercial computer software documentation" as such terms are used in 48 C.F.R. 12.212 (Sept. 1995). Consistent with 48 C.F.R. 12.212 and 48 C.F.R. 227.7202-1 through 227.7202-4 (June 1995), all U.S. Government End Users acquire Covered Software with only those rights set forth herein. This U.S. Government Rights clause is in lieu of, and supersedes, any other FAR, DFAR, or other clause or provision that addresses Government rights in computer software under this License.
-
-9. MISCELLANEOUS.
-
-     This License represents the complete agreement concerning subject matter hereof. If any provision of this License is held to be unenforceable, such provision shall be reformed only to the extent necessary to make it enforceable. This License shall be governed by the law of the jurisdiction specified in a notice contained within the Original Software (except to the extent applicable law, if any, provides otherwise), excluding such jurisdiction's conflict-of-law provisions. Any litigation relating to this License shall be subject to the jurisdiction of the courts located in the jurisdiction and venue specified in a notice contained within the Original Software, with the losing party responsible for costs, including, without limitation, court costs and reasonable attorneys' fees and expenses. The application of the United Nations Convention on Contracts for the International Sale of Goods is expressly excluded. Any law or regulation which provides that the language of a contract shall be construed against the drafter shall not apply to this License. You agree that You alone are responsible for compliance with the United States export administration regulations (and the export control laws and regulation of any other countries) when You use, distribute or otherwise make available any Covered Software.
-
-10. RESPONSIBILITY FOR CLAIMS.
-
-     As between Initial Developer and the Contributors, each party is responsible for claims and damages arising, directly or indirectly, out of its utilization of rights under this License and You agree to work with Initial Developer and Contributors to distribute such responsibility on an equitable basis. Nothing herein is intended or shall be deemed to constitute any admission of liability.
-
-----------
-NOTICE PURSUANT TO SECTION 9 OF THE COMMON DEVELOPMENT AND DISTRIBUTION LICENSE (CDDL)
-The code released under the CDDL shall be governed by the laws of the State of California (excluding conflict-of-law provisions). Any litigation relating to this License shall be subject to the jurisdiction of the Federal Courts of the Northern District of California and the state courts of the State of California, with venue lying in Santa Clara County, California.
-
diff --git a/guacamole/src/licenses/bundled/jstz-1.0.10/LICENSE b/guacamole/src/licenses/bundled/jstz-1.0.10/LICENSE
new file mode 100644
index 0000000..c48af16
--- /dev/null
+++ b/guacamole/src/licenses/bundled/jstz-1.0.10/LICENSE
@@ -0,0 +1,22 @@
+MIT License 
+
+Copyright (c) 2012 Jon Nylander, project maintained at 
+https://bitbucket.org/pellepim/jstimezonedetect
+
+Permission is hereby granted, free of charge, to any person obtaining a copy 
+of this software and associated documentation files (the "Software"), to deal 
+in the Software without restriction, including without limitation the rights to 
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
+of the Software, and to permit persons to whom the Software is furnished to 
+do so, subject to the following conditions: 
+
+The above copyright notice and this permission notice shall be included in 
+all copies or substantial portions of the Software. 
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
+THE SOFTWARE.
\ No newline at end of file
diff --git a/guacamole/src/licenses/bundled/pickr-1.2.6/LICENSE b/guacamole/src/licenses/bundled/pickr-1.2.6/LICENSE
new file mode 100644
index 0000000..e02b384
--- /dev/null
+++ b/guacamole/src/licenses/bundled/pickr-1.2.6/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2019 Simon Reinisch
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java b/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java
index a868931..6c6474b 100644
--- a/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java
+++ b/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java
@@ -19,14 +19,14 @@
 
 package org.apache.guacamole.extension;
 
+import java.util.Set;
 import java.util.UUID;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
 import org.apache.guacamole.net.auth.Credentials;
 import org.apache.guacamole.net.auth.UserContext;
-import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
-import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
+import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -49,6 +49,16 @@
     private final AuthenticationProvider authProvider;
 
     /**
+     * The set of identifiers of all authentication providers whose internal
+     * failures should be tolerated during the authentication process. If the
+     * identifier of this authentication provider is within this set, errors
+     * during authentication will result in the authentication provider being
+     * ignored for that authentication attempt. By default, errors during
+     * authentication halt the authentication process entirely.
+     */
+    private final Set<String> tolerateFailures;
+
+    /**
      * The identifier to provide for the underlying authentication provider if
      * the authentication provider could not be loaded.
      */
@@ -63,9 +73,21 @@
      *
      * @param authProviderClass
      *     The AuthenticationProvider subclass to instantiate.
+     *
+     * @param tolerateFailures
+     *     The set of identifiers of all authentication providers whose
+     *     internal failures should be tolerated during the authentication
+     *     process. If the identifier of this authentication provider is within
+     *     this set, errors during authentication will result in the
+     *     authentication provider being ignored for that authentication
+     *     attempt. By default, errors during authentication halt the
+     *     authentication process entirely.
      */
-    public AuthenticationProviderFacade(Class<? extends AuthenticationProvider> authProviderClass) {
-        authProvider = ProviderFactory.newInstance("authentication provider",
+    public AuthenticationProviderFacade(
+            Class<? extends AuthenticationProvider> authProviderClass,
+            Set<String> tolerateFailures) {
+        this.tolerateFailures = tolerateFailures;
+        this.authProvider = ProviderFactory.newInstance("authentication provider",
             authProviderClass);
     }
 
@@ -97,18 +119,124 @@
 
     }
 
+    /**
+     * Returns whether this authentication provider should tolerate internal
+     * failures during the authentication process, allowing other
+     * authentication providers to continue operating as if this authentication
+     * provider simply is not present.
+     *
+     * @return
+     *     true if this authentication provider should tolerate internal
+     *     failures during the authentication process, false otherwise.
+     */
+    private boolean isFailureTolerated() {
+        return tolerateFailures.contains(getIdentifier());
+    }
+
+    /**
+     * Logs a warning that this authentication provider is being skipped due to
+     * an internal error. If debug-level logging is enabled, the full details
+     * of the internal error are also logged.
+     *
+     * @param e
+     *     The internal error that occurred which has resulted in this
+     *     authentication provider being skipped.
+     */
+    private void warnAuthProviderSkipped(Throwable e) {
+
+        logger.warn("The \"{}\" authentication provider has been skipped due "
+                + "to an internal error. If this is unexpected or you are the "
+                + "developer of this authentication provider, you may wish to "
+                + "enable debug-level logging: {}",
+                getIdentifier(), e.getMessage());
+
+        logger.debug("Authentication provider skipped due to an internal failure.", e);
+
+    }
+
+    /**
+     * Logs a warning that the authentication process will be entirely aborted
+     * due to an internal error, advising the administrator to set the
+     * "skip-if-unavailable" property if error encountered is expected and
+     * should be tolerated.
+     */
+    private void warnAuthAborted() {
+        String identifier = getIdentifier();
+        logger.warn("The \"{}\" authentication provider has encountered an "
+                + "internal error which will halt the authentication "
+                + "process. If this is unexpected or you are the developer of "
+                + "this authentication provider, you may wish to enable "
+                + "debug-level logging. If this is expected and you wish to "
+                + "ignore such failures in the future, please set \"{}: {}\" "
+                + "within your guacamole.properties.",
+                identifier, ExtensionModule.SKIP_IF_UNAVAILABLE.getName(),
+                identifier);
+    }
+
     @Override
     public AuthenticatedUser authenticateUser(Credentials credentials)
             throws GuacamoleException {
 
         // Ignore auth attempts if no auth provider could be loaded
         if (authProvider == null) {
-            logger.warn("Authentication attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs.");
-            throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
+            logger.warn("Authentication attempt ignored because the relevant "
+                    + "authentication provider could not be loaded. Please "
+                    + "check for errors earlier in the logs.");
+            return null;
         }
 
         // Delegate to underlying auth provider
-        return authProvider.authenticateUser(credentials);
+        try {
+            return authProvider.authenticateUser(credentials);
+        }
+
+        // Pass through credential exceptions untouched, as these are not
+        // internal failures
+        catch (GuacamoleCredentialsException e) {
+            throw e;
+        }
+
+        // Pass through all other exceptions (aborting authentication entirely)
+        // only if not configured to ignore such failures
+        catch (GuacamoleException e) {
+
+            // Skip using this authentication provider if configured to ignore
+            // internal failures during auth
+            if (isFailureTolerated()) {
+                warnAuthProviderSkipped(e);
+                return null;
+            }
+
+            warnAuthAborted();
+            throw e;
+
+        }
+        catch (RuntimeException e) {
+
+            // Skip using this authentication provider if configured to ignore
+            // internal failures during auth
+            if (isFailureTolerated()) {
+                warnAuthProviderSkipped(e);
+                return null;
+            }
+
+            warnAuthAborted();
+            throw e;
+
+        }
+        catch (Error e) {
+
+            // Skip using this authentication provider if configured to ignore
+            // internal failures during auth
+            if (isFailureTolerated()) {
+                warnAuthProviderSkipped(e);
+                return null;
+            }
+
+            warnAuthAborted();
+            throw e;
+
+        }
 
     }
 
@@ -118,8 +246,10 @@
 
         // Ignore auth attempts if no auth provider could be loaded
         if (authProvider == null) {
-            logger.warn("Reauthentication attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs.");
-            throw new GuacamoleInvalidCredentialsException("Permission denied.", CredentialsInfo.USERNAME_PASSWORD);
+            logger.warn("Reauthentication attempt ignored because the relevant "
+                    + "authentication provider could not be loaded. Please "
+                    + "check for errors earlier in the logs.");
+            return null;
         }
 
         // Delegate to underlying auth provider
@@ -133,13 +263,65 @@
 
         // Ignore auth attempts if no auth provider could be loaded
         if (authProvider == null) {
-            logger.warn("User data retrieval attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs.");
+            logger.warn("User data retrieval attempt ignored because the "
+                    + "relevant authentication provider could not be loaded. "
+                    + "Please check for errors earlier in the logs.");
             return null;
         }
 
         // Delegate to underlying auth provider
-        return authProvider.getUserContext(authenticatedUser);
-        
+        try {
+            return authProvider.getUserContext(authenticatedUser);
+        }
+
+        // Pass through credential exceptions untouched, as these are not
+        // internal failures
+        catch (GuacamoleCredentialsException e) {
+            throw e;
+        }
+
+        // Pass through all other exceptions (aborting authentication entirely)
+        // only if not configured to ignore such failures
+        catch (GuacamoleException e) {
+
+            // Skip using this authentication provider if configured to ignore
+            // internal failures during auth
+            if (isFailureTolerated()) {
+                warnAuthProviderSkipped(e);
+                return null;
+            }
+
+            warnAuthAborted();
+            throw e;
+
+        }
+        catch (RuntimeException e) {
+
+            // Skip using this authentication provider if configured to ignore
+            // internal failures during auth
+            if (isFailureTolerated()) {
+                warnAuthProviderSkipped(e);
+                return null;
+            }
+
+            warnAuthAborted();
+            throw e;
+
+        }
+        catch (Error e) {
+
+            // Skip using this authentication provider if configured to ignore
+            // internal failures during auth
+            if (isFailureTolerated()) {
+                warnAuthProviderSkipped(e);
+                return null;
+            }
+
+            warnAuthAborted();
+            throw e;
+
+        }
+
     }
 
     @Override
@@ -149,7 +331,9 @@
 
         // Ignore auth attempts if no auth provider could be loaded
         if (authProvider == null) {
-            logger.warn("User data refresh attempt denied because the authentication system could not be loaded. Please check for errors earlier in the logs.");
+            logger.warn("User data refresh attempt ignored because the "
+                    + "relevant authentication provider could not be loaded. "
+                    + "Please check for errors earlier in the logs.");
             return null;
         }
 
diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
index cc39036..0a424ab 100644
--- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
+++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java
@@ -29,12 +29,14 @@
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import org.apache.guacamole.auth.file.FileAuthenticationProvider;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleServerException;
 import org.apache.guacamole.environment.Environment;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
 import org.apache.guacamole.net.event.listener.Listener;
+import org.apache.guacamole.properties.StringSetProperty;
 import org.apache.guacamole.resource.Resource;
 import org.apache.guacamole.resource.ResourceServlet;
 import org.apache.guacamole.resource.SequenceResource;
@@ -60,7 +62,8 @@
     private static final List<String> ALLOWED_GUACAMOLE_VERSIONS =
         Collections.unmodifiableList(Arrays.asList(
             "*",
-            "1.0.0"
+            "1.0.0",
+            "1.1.0"
         ));
 
     /**
@@ -82,6 +85,25 @@
     private static final String EXTENSION_SUFFIX = ".jar";
 
     /**
+     * A comma-separated list of the identifiers of all authentication
+     * providers whose internal failures should be tolerated during the
+     * authentication process. If an authentication provider within this list
+     * encounters an internal error during the authentication process, it will
+     * simply be skipped, allowing other authentication providers to continue
+     * trying to authenticate the user. Internal errors within authentication
+     * providers that are not within this list will halt the authentication
+     * process entirely.
+     */
+    public static final StringSetProperty SKIP_IF_UNAVAILABLE = new StringSetProperty() {
+
+        @Override
+        public String getName() {
+            return "skip-if-unavailable";
+        }
+
+    };
+
+    /**
      * The Guacamole server environment.
      */
     private final Environment environment;
@@ -156,13 +178,26 @@
      *
      * @param authenticationProvider
      *     The AuthenticationProvider class to bind.
+     *
+     * @param tolerateFailures
+     *     The set of identifiers of all authentication providers whose
+     *     internal failures should be tolerated during the authentication
+     *     process. If the identifier of an authentication provider is within
+     *     this set, errors during authentication will result in the
+     *     authentication provider being ignored for that authentication
+     *     attempt, with the authentication process proceeding as if that
+     *     authentication provider were not present. By default, errors during
+     *     authentication halt the authentication process entirely.
      */
-    private void bindAuthenticationProvider(Class<? extends AuthenticationProvider> authenticationProvider) {
+    private void bindAuthenticationProvider(
+            Class<? extends AuthenticationProvider> authenticationProvider,
+            Set<String> tolerateFailures) {
 
         // Bind authentication provider
         logger.debug("[{}] Binding AuthenticationProvider \"{}\".",
                 boundAuthenticationProviders.size(), authenticationProvider.getName());
-        boundAuthenticationProviders.add(new AuthenticationProviderFacade(authenticationProvider));
+        boundAuthenticationProviders.add(new AuthenticationProviderFacade(
+                authenticationProvider, tolerateFailures));
 
     }
 
@@ -173,12 +208,24 @@
      *
      * @param authProviders
      *     The AuthenticationProvider classes to bind.
+     *
+     * @param tolerateFailures
+     *     The set of identifiers of all authentication providers whose
+     *     internal failures should be tolerated during the authentication
+     *     process. If the identifier of an authentication provider is within
+     *     this set, errors during authentication will result in the
+     *     authentication provider being ignored for that authentication
+     *     attempt, with the authentication process proceeding as if that
+     *     authentication provider were not present. By default, errors during
+     *     authentication halt the authentication process entirely.
      */
-    private void bindAuthenticationProviders(Collection<Class<AuthenticationProvider>> authProviders) {
+    private void bindAuthenticationProviders(
+            Collection<Class<AuthenticationProvider>> authProviders,
+            Set<String> tolerateFailures) {
 
         // Bind each authentication provider within extension
         for (Class<AuthenticationProvider> authenticationProvider : authProviders)
-            bindAuthenticationProvider(authenticationProvider);
+            bindAuthenticationProvider(authenticationProvider, tolerateFailures);
 
     }
 
@@ -314,6 +361,38 @@
     }
 
     /**
+     * Returns the set of identifiers of all authentication providers whose
+     * internal failures should be tolerated during the authentication process.
+     * If the identifier of an authentication provider is within this set,
+     * errors during authentication will result in the authentication provider
+     * being ignored for that authentication attempt, with the authentication
+     * process proceeding as if that authentication provider were not present.
+     * By default, errors during authentication halt the authentication process
+     * entirely.
+     *
+     * @return
+     *     The set of identifiers of all authentication providers whose
+     *     internal failures should be tolerated during the authentication
+     *     process.
+     */
+    private Set<String> getToleratedAuthenticationProviders() {
+
+        // Parse list of auth providers whose internal failures should be
+        // tolerated
+        try {
+            return environment.getProperty(SKIP_IF_UNAVAILABLE, Collections.<String>emptySet());
+        }
+
+        // Use empty set by default if property cannot be parsed
+        catch (GuacamoleException e) {
+            logger.warn("The list of authentication providers specified via the \"{}\" property could not be parsed: {}", SKIP_IF_UNAVAILABLE.getName(), e.getMessage());
+            logger.debug("Unable to parse \"{}\" property.", SKIP_IF_UNAVAILABLE.getName(), e);
+            return Collections.<String>emptySet();
+        }
+
+    }
+
+    /**
      * Loads all extensions within the GUACAMOLE_HOME/extensions directory, if
      * any, adding their static resource to the given resoure collections.
      *
@@ -324,9 +403,20 @@
      * @param cssResources
      *     A modifiable collection of static CSS resources which may receive
      *     new CSS resources from extensions.
+     *
+     * @param toleratedAuthProviders
+     *     The set of identifiers of all authentication providers whose
+     *     internal failures should be tolerated during the authentication
+     *     process. If the identifier of an authentication provider is within
+     *     this set, errors during authentication will result in the
+     *     authentication provider being ignored for that authentication
+     *     attempt, with the authentication process proceeding as if that
+     *     authentication provider were not present. By default, errors during
+     *     authentication halt the authentication process entirely.
      */
     private void loadExtensions(Collection<Resource> javaScriptResources,
-            Collection<Resource> cssResources) {
+            Collection<Resource> cssResources,
+            Set<String> toleratedAuthProviders) {
 
         // Retrieve and validate extensions directory
         File extensionsDir = new File(environment.getGuacamoleHome(), EXTENSIONS_DIRECTORY);
@@ -375,7 +465,7 @@
                 cssResources.addAll(extension.getCSSResources().values());
 
                 // Attempt to load all authentication providers
-                bindAuthenticationProviders(extension.getAuthenticationProviderClasses());
+                bindAuthenticationProviders(extension.getAuthenticationProviderClasses(), toleratedAuthProviders);
 
                 // Attempt to load all listeners
                 bindListeners(extension.getListenerClasses());
@@ -430,10 +520,11 @@
         cssResources.add(new WebApplicationResource(getServletContext(), "/guacamole.min.css"));
 
         // Load all extensions
-        loadExtensions(javaScriptResources, cssResources);
+        final Set<String> toleratedAuthProviders = getToleratedAuthenticationProviders();
+        loadExtensions(javaScriptResources, cssResources, toleratedAuthProviders);
 
         // Always bind default file-driven auth last
-        bindAuthenticationProvider(FileAuthenticationProvider.class);
+        bindAuthenticationProvider(FileAuthenticationProvider.class, toleratedAuthProviders);
 
         // Dynamically generate app.js and app.css from extensions
         serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources)));
diff --git a/guacamole/src/main/java/org/apache/guacamole/net/auth/Connectable.java b/guacamole/src/main/java/org/apache/guacamole/net/auth/Connectable.java
new file mode 100644
index 0000000..e09baa1
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/net/auth/Connectable.java
@@ -0,0 +1,109 @@
+/*
+ * 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.net.auth;
+
+import java.util.Collections;
+import java.util.Map;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.GuacamoleTunnel;
+import org.apache.guacamole.protocol.GuacamoleClientInformation;
+
+/**
+ * Internal, ABI-compatible version of the Connectable interface from
+ * guacamole-ext which defines fallback defaults for older versions of the API.
+ * As this interface will take precedence in the servlet container's
+ * classloader over the definition from guacamole-ext, this allows backwards
+ * compatibility with the 1.0.0 API while keeping the actual API definition
+ * within guacamole-ext strict.
+ *
+ * <p>For this to work, this interface definition <strong>MUST</strong> be 100%
+ * ABI-compatible with the Connectable interface defined by guacamole-ext in
+ * 1.0.0 and onward.
+ */
+public interface Connectable {
+
+    /**
+     * Establishes a connection to guacd using the information associated with
+     * this object. The connection will be provided the given client
+     * information.
+     *
+     * <p>This definition is the legacy connect() definition from 1.0.0 and
+     * older. It is redefined here for the sake of ABI compatibility with
+     * 1.0.0 but is deprecated within guacamole-ext.
+     *
+     * @deprecated
+     *     This definition exists solely for binary compatibility. It should
+     *     never be used by new code. New implementations should instead use
+     *     the current version of connect() as defined by guacamole-ext.
+     *
+     * @param info
+     *     Information associated with the connecting client.
+     *
+     * @return
+     *     A fully-established GuacamoleTunnel.
+     *
+     * @throws GuacamoleException
+     *     If an error occurs while connecting to guacd, or if permission to
+     *     connect is denied.
+     */
+    @Deprecated
+    default GuacamoleTunnel connect(GuacamoleClientInformation info)
+            throws GuacamoleException {
+
+        // Pass through usages of the old API to the new API
+        return this.connect(info, Collections.emptyMap());
+
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This definition is the current version of connect() as defined by
+     * guacamole-ext.
+     *
+     * <p>A default implementation which invokes the old, deprecated
+     * {@link #connect(org.apache.guacamole.protocol.GuacamoleClientInformation)}
+     * is provided solely for compatibility with extensions which implement only
+     * the old version of this function. This default implementation is useful
+     * only for extensions relying on the older API and will be removed when
+     * support for that version of the API is removed.
+     *
+     * @see
+     *     The definition of getActiveConnections() in the current version of
+     *     the Connectable interface, as defined by guacamole-ext.
+     */
+    default GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+
+        // Allow old implementations of Connectable to continue to work
+        return this.connect(info);
+
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @see
+     *     The definition of getActiveConnections() in the current version of
+     *     the Connectable interface, as defined by guacamole-ext.
+     */
+    int getActiveConnections();
+    
+}
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java b/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java
index 0041a03..1634378 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/activeconnection/APIActiveConnection.java
@@ -55,6 +55,11 @@
     private final String username;
 
     /**
+     * Whether this active connection may be connected to.
+     */
+    private final boolean connectable;
+
+    /**
      * Creates a new APIActiveConnection, copying the information from the given
      * active connection.
      *
@@ -67,6 +72,7 @@
         this.startDate            = connection.getStartDate();
         this.remoteHost           = connection.getRemoteHost();
         this.username             = connection.getUsername();
+        this.connectable          = connection.isConnectable();
     }
 
     /**
@@ -121,5 +127,16 @@
     public String getIdentifier() {
         return identifier;
     }
-    
+
+    /***
+     * Returns whether this active connection may be connected to, just as a
+     * normal connection.
+     *
+     * @return
+     *     true if this active connection may be connected to, false otherwise.
+     */
+    public boolean isConnectable() {
+        return connectable;
+    }
+
 }
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/SecureRandomAuthTokenGenerator.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/SecureRandomAuthTokenGenerator.java
index ac09ff0..09a6340 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/SecureRandomAuthTokenGenerator.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/SecureRandomAuthTokenGenerator.java
@@ -19,8 +19,8 @@
 
 package org.apache.guacamole.rest.auth;
 
+import com.google.common.io.BaseEncoding;
 import java.security.SecureRandom;
-import javax.xml.bind.DatatypeConverter;
 
 /**
  * An implementation of the AuthTokenGenerator based around SecureRandom.
@@ -37,7 +37,7 @@
         byte[] bytes = new byte[32];
         secureRandom.nextBytes(bytes);
         
-        return DatatypeConverter.printHexBinary(bytes);
+        return BaseEncoding.base16().encode(bytes);
     }
     
 }
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/TokenRESTService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/TokenRESTService.java
index e1ff66f..887b4f0 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/TokenRESTService.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/TokenRESTService.java
@@ -19,6 +19,7 @@
 
 package org.apache.guacamole.rest.auth;
 
+import com.google.common.io.BaseEncoding;
 import com.google.inject.Inject;
 import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
@@ -33,7 +34,6 @@
 import javax.ws.rs.core.Context;
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
-import javax.xml.bind.DatatypeConverter;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.GuacamoleResourceNotFoundException;
 import org.apache.guacamole.net.auth.AuthenticatedUser;
@@ -94,7 +94,8 @@
 
                     // Decode base64 authorization
                     String basicBase64 = authorization.substring(6);
-                    String basicCredentials = new String(DatatypeConverter.parseBase64Binary(basicBase64), "UTF-8");
+                    String basicCredentials = new String(
+                            BaseEncoding.base64().decode(basicBase64), "UTF-8");
 
                     // Pull username/password from auth data
                     int colon = basicCredentials.indexOf(':');
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnectionWrapper.java b/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnectionWrapper.java
index 3a987e5..704db23 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnectionWrapper.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/connection/APIConnectionWrapper.java
@@ -128,7 +128,8 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException {
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
         throw new UnsupportedOperationException("Operation not supported.");
     }
 
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/APIConnectionGroupWrapper.java b/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/APIConnectionGroupWrapper.java
index 625b009..552b787 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/APIConnectionGroupWrapper.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/connectiongroup/APIConnectionGroupWrapper.java
@@ -112,7 +112,8 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException {
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
         throw new UnsupportedOperationException("Operation not supported.");
     }
 
diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/InputStreamInterceptingFilter.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/InputStreamInterceptingFilter.java
index 98c15c9..f8e0334 100644
--- a/guacamole/src/main/java/org/apache/guacamole/tunnel/InputStreamInterceptingFilter.java
+++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/InputStreamInterceptingFilter.java
@@ -19,11 +19,11 @@
 
 package org.apache.guacamole.tunnel;
 
+import com.google.common.io.BaseEncoding;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.Arrays;
 import java.util.List;
-import javax.xml.bind.DatatypeConverter;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.net.GuacamoleTunnel;
 import org.apache.guacamole.protocol.GuacamoleInstruction;
@@ -75,7 +75,7 @@
 
         // Send "blob" containing provided data
         sendInstruction(new GuacamoleInstruction("blob", index,
-            DatatypeConverter.printBase64Binary(blob)));
+           BaseEncoding.base64().encode(blob)));
 
     }
 
diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/OutputStreamInterceptingFilter.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/OutputStreamInterceptingFilter.java
index 85ae02c..99cf153 100644
--- a/guacamole/src/main/java/org/apache/guacamole/tunnel/OutputStreamInterceptingFilter.java
+++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/OutputStreamInterceptingFilter.java
@@ -19,10 +19,10 @@
 
 package org.apache.guacamole.tunnel;
 
+import com.google.common.io.BaseEncoding;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.util.List;
-import javax.xml.bind.DatatypeConverter;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.net.GuacamoleTunnel;
 import org.apache.guacamole.protocol.GuacamoleInstruction;
@@ -127,7 +127,7 @@
         byte[] blob;
         try {
             String data = args.get(1);
-            blob = DatatypeConverter.parseBase64Binary(data);
+            blob = BaseEncoding.base64().decode(data);
         }
         catch (IllegalArgumentException e) {
             logger.warn("Received base64 data for intercepted stream was invalid.");
diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/StandardTokenMap.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/StandardTokenMap.java
new file mode 100644
index 0000000..3392861
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/StandardTokenMap.java
@@ -0,0 +1,122 @@
+/*
+ * 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 java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import org.apache.guacamole.net.auth.AuthenticatedUser;
+import org.apache.guacamole.net.auth.Credentials;
+
+/**
+ * Map which is automatically populated with the name/value pairs of all
+ * standardized tokens available for a particular AuthenticatedUser.
+ */
+public class StandardTokenMap extends HashMap<String, String> {
+
+    /**
+     * The name of the token containing the user's username.
+     */
+    public static final String USERNAME_TOKEN = "GUAC_USERNAME";
+
+    /**
+     * The name of the token containing the user's password.
+     */
+    public static final String PASSWORD_TOKEN = "GUAC_PASSWORD";
+
+    /**
+     * The name of the token containing the hostname/address of the machine the
+     * user authenticated from.
+     */
+    public static final String CLIENT_HOSTNAME_TOKEN = "GUAC_CLIENT_HOSTNAME";
+
+    /**
+     * The name of the token containing the IP address of the machine the user
+     * authenticated from.
+     */
+    public static final String CLIENT_ADDRESS_TOKEN = "GUAC_CLIENT_ADDRESS";
+
+    /**
+     * The name of the token containing the current date (server-local time).
+     */
+    public static final String DATE_TOKEN = "GUAC_DATE";
+
+    /**
+     * The name of the token containing the current time (server-local time).
+     */
+    public static final String TIME_TOKEN = "GUAC_TIME";
+
+    /**
+     * The date format that should be used for the date token. This format must
+     * be compatible with Java's SimpleDateFormat.
+     */
+    private static final String DATE_FORMAT = "yyyyMMdd";
+
+    /**
+     * The date format that should be used for the time token. This format must
+     * be compatible with Java's SimpleDateFormat.
+     */
+    private static final String TIME_FORMAT = "HHmmss";
+
+    /**
+     * Creates a new StandardTokenMap which is pre-populated with the
+     * name/value pairs of all standardized tokens available for the given
+     * AuthenticatedUser.
+     *
+     * @param authenticatedUser
+     *     The AuthenticatedUser to generate standard tokens for.
+     */
+    public StandardTokenMap(AuthenticatedUser authenticatedUser) {
+
+        // Add date/time tokens (server-local time)
+        Date currentTime = new Date();
+        put(DATE_TOKEN, new SimpleDateFormat(DATE_FORMAT).format(currentTime));
+        put(TIME_TOKEN, new SimpleDateFormat(TIME_FORMAT).format(currentTime));
+
+        Credentials credentials = authenticatedUser.getCredentials();
+
+        // Add username token
+        String username = credentials.getUsername();
+        if (username != null)
+            put(USERNAME_TOKEN, username);
+
+        // Default to the authenticated user's username for the GUAC_USERNAME
+        // token
+        else
+            put(USERNAME_TOKEN, authenticatedUser.getIdentifier());
+
+        // Add password token
+        String password = credentials.getPassword();
+        if (password != null)
+            put(PASSWORD_TOKEN, password);
+
+        // Add client hostname token
+        String hostname = credentials.getRemoteHostname();
+        if (hostname != null)
+            put(CLIENT_HOSTNAME_TOKEN, hostname);
+
+        // Add client address token
+        String address = credentials.getRemoteAddress();
+        if (address != null)
+            put(CLIENT_ADDRESS_TOKEN, address);
+
+    }
+
+}
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 e023a70..fa56b19 100644
--- a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java
+++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java
@@ -22,16 +22,15 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 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;
@@ -165,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;
     }
@@ -187,6 +191,10 @@
      * @param info
      *     Information describing the connected Guacamole client.
      *
+     * @param tokens
+     *     A Map containing the token names and corresponding values to be
+     *     applied as parameter tokens when establishing the connection.
+     *
      * @return
      *     A new tunnel, connected as required by the request.
      *
@@ -194,58 +202,20 @@
      *     If an error occurs while creating the tunnel.
      */
     protected GuacamoleTunnel createConnectedTunnel(UserContext context,
-            final TunnelRequest.Type type, String id,
-            GuacamoleClientInformation info)
+            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);
-                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);
-                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;
 
     }
@@ -287,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
@@ -310,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 {
 
@@ -380,21 +333,22 @@
         // 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);
 
         GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
+        AuthenticatedUser authenticatedUser = session.getAuthenticatedUser();
         UserContext userContext = session.getUserContext(authProviderIdentifier);
 
         try {
 
             // Create connected tunnel using provided connection ID and client information
-            GuacamoleTunnel tunnel = createConnectedTunnel(userContext, type, id, info);
+            GuacamoleTunnel tunnel = createConnectedTunnel(userContext, type,
+                    id, info, new StandardTokenMap(authenticatedUser));
 
             // Notify listeners to allow connection to be vetoed
-            fireTunnelConnectEvent(session.getAuthenticatedUser(),
-                    session.getAuthenticatedUser().getCredentials(), tunnel);
+            fireTunnelConnectEvent(authenticatedUser, authenticatedUser.getCredentials(), tunnel);
 
             // Associate tunnel with session
             return createAssociatedTunnel(tunnel, authToken, session, userContext, type, id);
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/auth/service/authenticationService.js b/guacamole/src/main/webapp/app/auth/service/authenticationService.js
index 53b10c6..d05a0d3 100644
--- a/guacamole/src/main/webapp/app/auth/service/authenticationService.js
+++ b/guacamole/src/main/webapp/app/auth/service/authenticationService.js
@@ -205,6 +205,10 @@
             else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS)
                 $rootScope.$broadcast('guacInsufficientCredentials', parameters, error);
 
+            // Abort rendering of page if an internal error occurs
+            else if (error.type === Error.Type.INTERNAL_ERROR)
+                $rootScope.$broadcast('guacFatalPageError', error);
+
             // Authentication failed
             throw error;
 
diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js
index 41c6ba6..c8941b3 100644
--- a/guacamole/src/main/webapp/app/client/controllers/clientController.js
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -24,22 +24,26 @@
         function clientController($scope, $routeParams, $injector) {
 
     // Required types
+    var ConnectionGroup    = $injector.get('ConnectionGroup');
     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
-    var $location             = $injector.get('$location');
-    var authenticationService = $injector.get('authenticationService');
-    var clipboardService      = $injector.get('clipboardService');
-    var guacClientManager     = $injector.get('guacClientManager');
-    var guacNotification      = $injector.get('guacNotification');
-    var iconService           = $injector.get('iconService');
-    var preferenceService     = $injector.get('preferenceService');
-    var requestService        = $injector.get('requestService');
-    var tunnelService         = $injector.get('tunnelService');
-    var userPageService       = $injector.get('userPageService');
+    var $location              = $injector.get('$location');
+    var authenticationService  = $injector.get('authenticationService');
+    var connectionGroupService = $injector.get('connectionGroupService');
+    var clipboardService       = $injector.get('clipboardService');
+    var dataSourceService      = $injector.get('dataSourceService');
+    var guacClientManager      = $injector.get('guacClientManager');
+    var guacNotification       = $injector.get('guacNotification');
+    var iconService            = $injector.get('iconService');
+    var preferenceService      = $injector.get('preferenceService');
+    var requestService         = $injector.get('requestService');
+    var tunnelService          = $injector.get('tunnelService');
+    var userPageService        = $injector.get('userPageService');
 
     /**
      * The minimum number of pixels a drag gesture must move to result in the
@@ -248,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 : {}
 
     };
 
@@ -258,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
@@ -265,6 +287,64 @@
     $scope.client = guacClientManager.getManagedClient($routeParams.id, $routeParams.params);
 
     /**
+     * All active clients which are not the current client ($scope.client).
+     * Each key is the ID of the connection used by that client.
+     *
+     * @type Object.<String, ManagedClient>
+     */
+    $scope.otherClients = (function getOtherClients(clients) {
+        var otherClients = angular.extend({}, clients);
+        delete otherClients[$scope.client.id];
+        return otherClients;
+    })(guacClientManager.getManagedClients());
+
+    /**
+     * 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>
+     */
+    $scope.rootConnectionGroups = null;
+
+    /**
+     * Array of all connection properties that are filterable.
+     *
+     * @type String[]
+     */
+    $scope.filteredConnectionProperties = [
+        'name'
+    ];
+
+    /**
+     * Array of all connection group properties that are filterable.
+     *
+     * @type String[]
+     */
+    $scope.filteredConnectionGroupProperties = [
+        'name'
+    ];
+
+    // Retrieve root groups and all descendants
+    dataSourceService.apply(
+        connectionGroupService.getConnectionGroupTree,
+        authenticationService.getAvailableDataSources(),
+        ConnectionGroup.ROOT_IDENTIFIER
+    )
+    .then(function rootGroupsRetrieved(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);
+
+    /**
      * Map of all available sharing profiles for the current connection by
      * their identifiers. If this information is not yet available, or no such
      * sharing profiles exist, this will be an empty object.
@@ -429,17 +509,31 @@
 
     });
 
+    // 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;
 
     });
 
+    // Update last used timestamp when the active client changes
+    $scope.$watch('client', function clientChanged(client) {
+        if (client)
+            client.lastUsed = new Date().getTime();
+    });
+
     // Update page icon when thumbnail changes
     $scope.$watch('client.thumbnail.canvas', function thumbnailChanged(canvas) {
         iconService.setIcons(canvas);
@@ -806,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/directives/guacClient.js b/guacamole/src/main/webapp/app/client/directives/guacClient.js
index 769edd7..cc88296 100644
--- a/guacamole/src/main/webapp/app/client/directives/guacClient.js
+++ b/guacamole/src/main/webapp/app/client/directives/guacClient.js
@@ -282,6 +282,9 @@
                     return false;
                 };
 
+                // Size of newly-attached client may be different
+                $scope.mainElementResized();
+
             });
 
             // Update actual view scrollLeft when scroll properties change
diff --git a/guacamole/src/main/webapp/app/client/directives/guacClientPanel.js b/guacamole/src/main/webapp/app/client/directives/guacClientPanel.js
new file mode 100644
index 0000000..658be2f
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/directives/guacClientPanel.js
@@ -0,0 +1,170 @@
+/*
+ * 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 toolbar/panel which displays a list of active Guacamole connections. The
+ * panel is fixed to the bottom-right corner of its container and can be
+ * manually hidden/exposed by the user.
+ */
+angular.module('client').directive('guacClientPanel', ['$injector', function guacClientPanel($injector) {
+
+    // Required services
+    var guacClientManager     = $injector.get('guacClientManager');
+    var sessionStorageFactory = $injector.get('sessionStorageFactory');
+
+    // Required types
+    var ManagedClientState = $injector.get('ManagedClientState');
+
+    /**
+     * Getter/setter for the boolean flag controlling whether the client panel
+     * is currently hidden. This flag is maintained in session-local storage to
+     * allow the state of the panel to persist despite navigation within the
+     * same tab. When hidden, the panel will be collapsed against the right
+     * side of the container. By default, the panel is visible.
+     *
+     * @type Function
+     */
+    var panelHidden = sessionStorageFactory.create(false);
+
+    return {
+        // Element only
+        restrict: 'E',
+        replace: true,
+        scope: {
+
+            /**
+             * The ManagedClient instances associated with the active
+             * connections to be displayed within this panel.
+             * 
+             * @type ManagedClient[]|Object.<String, ManagedClient>
+             */
+            clients : '='
+
+        },
+        templateUrl: 'app/client/templates/guacClientPanel.html',
+        controller: ['$scope', '$element', function guacClientPanelController($scope, $element) {
+
+            /**
+             * The DOM element containing the scrollable portion of the client
+             * panel.
+             *
+             * @type Element
+             */
+            var scrollableArea = $element.find('.client-panel-connection-list')[0];
+
+            /**
+             * On-scope reference to session-local storage of the flag
+             * controlling whether then panel is hidden.
+             */
+            $scope.panelHidden = panelHidden;
+
+            /**
+             * Returns whether this panel currently has any clients associated
+             * with it.
+             *
+             * @return {Boolean}
+             *     true if at least one client is associated with this panel,
+             *     false otherwise.
+             */
+            $scope.hasClients = function hasClients() {
+                return !!_.find($scope.clients, $scope.isManaged);
+            };
+
+            /**
+             * Returns whether the status of the given client has changed in a
+             * way that requires the user's attention. This may be due to an
+             * error, or due to a server-initiated disconnect.
+             *
+             * @param {ManagedClient} client
+             *     The client to test.
+             *
+             * @returns {Boolean}
+             *     true if the given client requires the user's attention,
+             *     false otherwise.
+             */
+            $scope.hasStatusUpdate = function hasStatusUpdate(client) {
+
+                // Test whether the client has encountered an error
+                switch (client.clientState.connectionState) {
+                    case ManagedClientState.ConnectionState.CONNECTION_ERROR:
+                    case ManagedClientState.ConnectionState.TUNNEL_ERROR:
+                    case ManagedClientState.ConnectionState.DISCONNECTED:
+                        return true;
+                }
+
+                return false;
+
+            };
+
+            /**
+             * Returns whether the given client is currently being managed by
+             * the guacClientManager service.
+             *
+             * @param {ManagedClient} client
+             *     The client to test.
+             *
+             * @returns {Boolean}
+             *     true if the given client is being managed by the
+             *     guacClientManager service, false otherwise.
+             */
+            $scope.isManaged = function isManaged(client) {
+                return !!guacClientManager.getManagedClients()[client.id];
+            };
+
+            /**
+             * Initiates an orderly disconnect of the given client. The client
+             * is removed from management such that attempting to connect to
+             * the same connection will result in a new connection being
+             * established, rather than displaying a notification that the
+             * connection has ended.
+             *
+             * @param {type} client
+             * @returns {undefined}
+             */
+            $scope.disconnect = function disconnect(client) {
+                client.client.disconnect();
+                guacClientManager.removeManagedClient(client.id);
+            };
+
+            /**
+             * Toggles whether the client panel is currently hidden.
+             */
+            $scope.togglePanel = function togglePanel() {
+                panelHidden(!panelHidden());
+            };
+
+            // Override vertical scrolling, scrolling horizontally instead
+            scrollableArea.addEventListener('wheel', function reorientVerticalScroll(e) {
+
+                var deltaMultiplier = {
+                    /* DOM_DELTA_PIXEL */ 0x00: 1,
+                    /* DOM_DELTA_LINE  */ 0x01: 15,
+                    /* DOM_DELTA_PAGE  */ 0x02: scrollableArea.offsetWidth
+                };
+
+                if (e.deltaY) {
+                    this.scrollLeft += e.deltaY * (deltaMultiplier[e.deltaMode] || deltaMultiplier(0x01));
+                    e.preventDefault();
+                }
+
+            });
+
+        }]
+    };
+}]);
\ No newline at end of file
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 9e0fb7c..aa80e09 100644
--- a/guacamole/src/main/webapp/app/client/styles/guac-menu.css
+++ b/guacamole/src/main/webapp/app/client/styles/guac-menu.css
@@ -65,6 +65,13 @@
     margin-top: 1em;
 }
 
+#guac-menu .header h2 {
+    white-space: nowrap;
+    overflow: hidden;
+    width: 100%;
+    text-overflow: ellipsis;
+}
+
 #guac-menu #mouse-settings .choice {
     text-align: center;
 }
diff --git a/guacamole/src/main/webapp/app/client/styles/keyboard.css b/guacamole/src/main/webapp/app/client/styles/keyboard.css
index 8076d54..e5bb963 100644
--- a/guacamole/src/main/webapp/app/client/styles/keyboard.css
+++ b/guacamole/src/main/webapp/app/client/styles/keyboard.css
@@ -18,6 +18,8 @@
  */
 
 .keyboard-container {
+
+    display: none;
     text-align: center;
 
     width: 100%;
@@ -29,4 +31,9 @@
     opacity: 0.85;
 
     z-index: 1;
+
 }
+
+.keyboard-container.open {
+    display: block;
+}
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/client/styles/other-connections.css b/guacamole/src/main/webapp/app/client/styles/other-connections.css
new file mode 100644
index 0000000..6c57aaa
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/styles/other-connections.css
@@ -0,0 +1,206 @@
+/*
+ * 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.
+ */
+
+#other-connections .client-panel {
+
+    display: none;
+    position: absolute;
+    right: 0;
+    bottom: 0;
+
+    border: 1px solid rgba(255, 255, 255, 0.25);
+    background: rgba(0, 0, 0, 0.25);
+    max-width: 100%;
+    white-space: nowrap;
+    transition: max-width 0.125s, width 0.125s;
+
+    /* Render above modal status */
+    z-index: 20;
+
+}
+
+#other-connections .client-panel.has-clients {
+    display: block;
+}
+
+#other-connections .client-panel.hidden {
+    max-width: 16px;
+}
+
+#other-connections .client-panel-handle {
+
+    position: absolute;
+    left: 0;
+    bottom: 0;
+    height: 100%;
+    width: 16px;
+    z-index: 1;
+
+    background-color: white;
+    background-repeat: no-repeat;
+    background-size: contain;
+    background-position: center center;
+    background-image: url(images/arrows/right.png);
+    opacity: 0.5;
+
+}
+
+#other-connections .client-panel-handle:hover {
+    opacity: 0.75;
+}
+
+#other-connections .client-panel.hidden .client-panel-handle {
+    background-image: url(images/arrows/left.png);
+}
+
+#other-connections .client-panel-connection-list {
+
+    text-align: right;
+
+    margin: 0;
+    padding: 0;
+    padding-left: 16px;
+
+    overflow-x: auto;
+    overflow-y: hidden;
+
+}
+
+#other-connections .client-panel-connection {
+
+    display: inline-block;
+    position: relative;
+
+    margin: 0.5em;
+    border: 1px solid white;
+    background: black;
+    box-shadow: 1px 1px 3px rgba(0, 0, 0, 0.5);
+
+    opacity: 0.5;
+    transition: opacity 0.25s;
+
+    max-height: 128px;
+    overflow: hidden;
+    vertical-align: middle;
+
+}
+
+#other-connections .client-panel-connection .thumbnail-main img {
+    max-width: none;
+    max-height: 128px;
+}
+
+#other-connections .client-panel-connection a[href]::before {
+
+    display: block;
+    content: ' ';
+
+    position: absolute;
+    top: 0;
+    left: 0;
+    height: 100%;
+    width: 100%;
+    z-index: 1;
+
+    background: url('images/warning-white.png');
+    background-size: 48px;
+    background-position: center;
+    background-repeat: no-repeat;
+    background-color: black;
+
+    opacity: 0;
+    transition: opacity 0.25s;
+
+}
+
+#other-connections .client-panel-connection.needs-attention a[href]::before {
+    opacity: 0.75;
+}
+
+#other-connections button.close-other-connection {
+
+    position: absolute;
+    top: 0;
+    right: 0;
+    z-index: 2;
+
+    margin: 0;
+    padding: 4px;
+    min-width: 0;
+    border: none;
+    background: transparent;
+    box-shadow: none;
+    text-shadow: none;
+
+    opacity: 0.5;
+    line-height: 1;
+
+}
+
+#other-connections button.close-other-connection:hover {
+    opacity: 1;
+}
+
+#other-connections button.close-other-connection img {
+    background: #A43;
+    border-radius: 18px;
+    max-width: 18px;
+    padding: 3px;
+}
+
+#other-connections button.close-other-connection:hover img {
+    background: #C54;
+}
+
+#other-connections .client-panel.hidden .client-panel-connection-list {
+    /* Hide scrollbar when panel is hidden (will be visible through panel
+     * show/hide button otherwise) */
+    overflow-x: hidden;
+}
+
+#other-connections .client-panel.hidden .client-panel-connection {
+    /* Hide thumbnails when panel is hidden (will be visible through panel
+     * show/hide button otherwise) */
+    visibility: hidden;
+}
+
+#other-connections .client-panel-connection .name {
+
+    position: absolute;
+    padding: 0.25em 0.5em;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    z-index: 2;
+
+    text-align: left;
+    color: white;
+    background: rgba(0, 0, 0, 0.5);
+    font-size: 0.75em;
+    font-weight: bold;
+
+    white-space: nowrap;
+    overflow: hidden;
+    text-overflow: ellipsis;
+
+}
+
+#other-connections .client-panel-connection:hover {
+    opacity: 1;
+}
diff --git a/guacamole/src/main/webapp/app/index/config/indexHttpPatchConfig.js b/guacamole/src/main/webapp/app/client/styles/text-input.css
similarity index 74%
rename from guacamole/src/main/webapp/app/index/config/indexHttpPatchConfig.js
rename to guacamole/src/main/webapp/app/client/styles/text-input.css
index 114d598..28e905e 100644
--- a/guacamole/src/main/webapp/app/index/config/indexHttpPatchConfig.js
+++ b/guacamole/src/main/webapp/app/client/styles/text-input.css
@@ -17,15 +17,10 @@
  * under the License.
  */
 
-/**
- * The config block for setting up the HTTP PATCH method.
- */
-angular.module('index').config(['$httpProvider', 
-        function indexHttpPatchConfig($httpProvider) {
-    
-    $httpProvider.defaults.headers.patch = {
-        'Content-Type': 'application/json'
-    }
-}]);
+.text-input-container {
+    display: none;
+}
 
-
+.text-input-container.open {
+    display: block;
+}
diff --git a/guacamole/src/main/webapp/app/client/templates/client.html b/guacamole/src/main/webapp/app/client/templates/client.html
index ad85f23..5325b47 100644
--- a/guacamole/src/main/webapp/app/client/templates/client.html
+++ b/guacamole/src/main/webapp/app/client/templates/client.html
@@ -8,21 +8,26 @@
             <!-- Central portion of view -->
             <div class="client-body" guac-touch-drag="clientDrag" guac-touch-pinch="clientPinch">
 
-                <!-- Client -->
+                <!-- Client for current connection -->
                 <guac-client client="client"></guac-client>
 
+                <!-- All other active connections -->
+                <div id="other-connections">
+                    <guac-client-panel clients="otherClients"></guac-client-panel>
+                </div>
+
             </div>
 
             <!-- Bottom portion of view -->
             <div class="client-bottom">
 
                 <!-- Text input -->
-                <div class="text-input-container" ng-show="showTextInput">
+                <div class="text-input-container" ng-class="{ open : showTextInput }">
                     <guac-text-input needs-focus="showTextInput"></guac-text-input>
                 </div>
 
                 <!-- On-screen keyboard -->
-                <div class="keyboard-container" ng-show="showOSK">
+                <div class="keyboard-container" ng-class="{ open : showOSK }">
                     <guac-osk layout="'CLIENT.URL_OSK_LAYOUT' | translate"></guac-osk>
                 </div>
 
@@ -47,7 +52,25 @@
 
             <!-- Stationary header -->
             <div class="header">
-                <h2>{{client.name}}</h2>
+                <h2 ng-hide="rootConnectionGroups">{{client.name}}</h2>
+                <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"
+                                placeholder="'CLIENT.FIELD_PLACEHOLDER_FILTER' | translate"
+                                connection-properties="filteredConnectionProperties"
+                                connection-group-properties="filteredConnectionGroupProperties"></guac-group-list-filter>
+                            <guac-group-list
+                                connection-groups="filteredRootConnectionGroups"
+                                templates="{
+                                    'connection'       : 'app/client/templates/connection.html',
+                                    'connection-group' : 'app/client/templates/connectionGroup.html'
+                                }"
+                                page-size="10"></guac-group-list>
+                        </div>
+                    </guac-menu>
+                </h2>
                 <div class="share-menu" ng-show="canShareConnection()">
                     <guac-menu menu-title="'CLIENT.ACTION_SHARE' | translate">
                         <ul ng-repeat="sharingProfile in sharingProfiles">
@@ -96,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/templates/connection.html b/guacamole/src/main/webapp/app/client/templates/connection.html
new file mode 100644
index 0000000..f6c1e70
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/templates/connection.html
@@ -0,0 +1,4 @@
+<a class="connection" ng-href="#/client/{{ item.getClientIdentifier() }}">
+    <div class="icon type" ng-class="item.protocol"></div>
+    <span class="name">{{item.name}}</span>
+</a>
diff --git a/guacamole/src/main/webapp/app/client/templates/connectionGroup.html b/guacamole/src/main/webapp/app/client/templates/connectionGroup.html
new file mode 100644
index 0000000..b2ea62c
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/templates/connectionGroup.html
@@ -0,0 +1,4 @@
+<span class="connection-group name">
+    <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/client/templates/guacClientPanel.html b/guacamole/src/main/webapp/app/client/templates/guacClientPanel.html
new file mode 100644
index 0000000..bb6e7e3
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/templates/guacClientPanel.html
@@ -0,0 +1,32 @@
+<div class="client-panel"
+     ng-class="{ 'has-clients': hasClients(), 'hidden' : panelHidden() }">
+
+    <!-- Toggle panel visibility -->
+    <div class="client-panel-handle" ng-click="togglePanel()"></div>
+
+    <!-- List of connection thumbnails -->
+    <ul class="client-panel-connection-list">
+        <li ng-repeat="client in clients | toArray | orderBy: [ '-value.lastUsed', 'value.title' ]"
+            ng-class="{ 'needs-attention' : hasStatusUpdate(client.value) }"
+            ng-show="isManaged(client.value)"
+            class="client-panel-connection">
+
+            <!-- Close connection -->
+            <button class="close-other-connection" ng-click="disconnect(client.value)">
+                <img ng-attr-alt="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
+                     ng-attr-title="{{ 'CLIENT.ACTION_DISCONNECT' | translate }}"
+                     src="images/x.png">
+            </button>
+
+            <!-- Thumbnail -->
+            <a href="#/client/{{client.value.id}}">
+                <div class="thumbnail">
+                    <guac-thumbnail client="client.value"></guac-thumbnail>
+                </div>
+                <div class="name">{{ client.value.title }}</div>
+            </a>
+
+        </li>
+    </ul>
+
+</div>
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedArgument.js b/guacamole/src/main/webapp/app/client/types/ManagedArgument.js
new file mode 100644
index 0000000..247d9f6
--- /dev/null
+++ b/guacamole/src/main/webapp/app/client/types/ManagedArgument.js
@@ -0,0 +1,152 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/**
+ * Provides the ManagedArgument class used by ManagedClient.
+ */
+angular.module('client').factory('ManagedArgument', ['$q', function defineManagedArgument($q) {
+
+    /**
+     * Object which represents an argument (connection parameter) which may be
+     * changed by the user while the connection is open.
+     * 
+     * @constructor
+     * @param {ManagedArgument|Object} [template={}]
+     *     The object whose properties should be copied within the new
+     *     ManagedArgument.
+     */
+    var ManagedArgument = function ManagedArgument(template) {
+
+        // Use empty object by default
+        template = template || {};
+
+        /**
+         * The name of the connection parameter.
+         *
+         * @type {String}
+         */
+        this.name = template.name;
+
+        /**
+         * The current value of the connection parameter.
+         *
+         * @type {String}
+         */
+        this.value = template.value;
+
+        /**
+         * A valid, open output stream which may be used to apply a new value
+         * to the connection parameter.
+         *
+         * @type {Guacamole.OutputStream}
+         */
+        this.stream = template.stream;
+
+    };
+
+    /**
+     * Requests editable access to a given connection parameter, returning a
+     * promise which is resolved with a ManagedArgument instance that provides
+     * such access if the parameter is indeed editable.
+     *
+     * @param {ManagedClient} managedClient
+     *     The ManagedClient instance associated with the connection for which
+     *     an editable version of the connection parameter is being retrieved.
+     *
+     * @param {String} name
+     *     The name of the connection parameter.
+     *
+     * @param {String} value
+     *     The current value of the connection parameter, as received from a
+     *     prior, inbound "argv" stream.
+     *
+     * @returns {Promise.<ManagedArgument>}
+     *     A promise which is resolved with the new ManagedArgument instance
+     *     once the requested parameter has been verified as editable.
+     */
+    ManagedArgument.getInstance = function getInstance(managedClient, name, value) {
+
+        var deferred = $q.defer();
+
+        // Create internal, fully-populated instance of ManagedArgument, to be
+        // returned only once mutability of the associated connection parameter
+        // has been verified
+        var managedArgument = new ManagedArgument({
+            name   : name,
+            value  : value,
+            stream : managedClient.client.createArgumentValueStream('text/plain', name)
+        });
+
+        // The connection parameter is editable only if a successful "ack" is
+        // received
+        managedArgument.stream.onack = function ackReceived(status) {
+            if (status.isError())
+                deferred.reject(status);
+            else
+                deferred.resolve(managedArgument);
+        };
+
+        return deferred.promise;
+
+    };
+
+    /**
+     * Sets the given editable argument (connection parameter) to the given
+     * value, updating the behavior of the associated connection in real-time.
+     * If successful, the ManagedArgument provided cannot be used for future
+     * calls to setValue() and must be replaced with a new instance. This
+     * function only has an effect if the new parameter value is different from
+     * the current value.
+     *
+     * @param {ManagedArgument} managedArgument
+     *     The ManagedArgument instance associated with the connection
+     *     parameter being modified.
+     *
+     * @param {String} value
+     *     The new value to assign to the connection parameter.
+     *
+     * @returns {Boolean}
+     *     true if the connection parameter was sent and the provided
+     *     ManagedArgument instance may no longer be used for future setValue()
+     *     calls, false if the connection parameter was NOT sent as it has not
+     *     changed.
+     */
+    ManagedArgument.setValue = function setValue(managedArgument, value) {
+
+        // Stream new value only if value has changed
+        if (value !== managedArgument.value) {
+
+            var writer = new Guacamole.StringWriter(managedArgument.stream);
+            writer.sendText(value);
+            writer.sendEnd();
+
+            // ManagedArgument instance is no longer usable
+            return true;
+
+        }
+
+        // No parameter value change was attempted and the ManagedArgument
+        // instance may be reused
+        return false;
+
+    };
+
+    return ManagedArgument;
+
+}]);
\ No newline at end of file
diff --git a/guacamole/src/main/webapp/app/client/types/ManagedClient.js b/guacamole/src/main/webapp/app/client/types/ManagedClient.js
index a9bc3be..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
@@ -80,6 +84,16 @@
         this.id = template.id;
 
         /**
+         * The time that the connection was last brought to the foreground of
+         * the current tab, as the number of milliseconds elapsed since
+         * midnight of January 1, 1970 UTC. If the connection has not yet been
+         * viewed, this will be 0.
+         *
+         * @type Number
+         */
+        this.lastUsed = template.lastUsed || 0;
+
+        /**
          * The actual underlying Guacamole client.
          *
          * @type Guacamole.Client
@@ -117,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.
@@ -178,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 || {};
+
     };
 
     /**
@@ -225,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
@@ -446,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) {
 
@@ -520,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);
         }
         
@@ -536,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;
 
     };
@@ -618,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 9cf785c..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:
              *
@@ -119,7 +146,7 @@
             $scope.getFieldOption = function getFieldOption(value) {
 
                 // If no field, or no value, then no corresponding translation string
-                if (!$scope.field || !$scope.field.name || !value)
+                if (!$scope.field || !$scope.field.name)
                     return '';
 
                 return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE')
@@ -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/groupList/directives/guacGroupList.js b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js
index a0515ea..16ff9dd 100644
--- a/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js
+++ b/guacamole/src/main/webapp/app/groupList/directives/guacGroupList.js
@@ -222,7 +222,7 @@
                             });
                         });
 
-                    }, requestService.WARN);
+                    }, requestService.DIE);
 
                 }
 
diff --git a/guacamole/src/main/webapp/app/groupList/directives/guacGroupListFilter.js b/guacamole/src/main/webapp/app/groupList/directives/guacGroupListFilter.js
index 5a3c6b5..ad1bd43 100644
--- a/guacamole/src/main/webapp/app/groupList/directives/guacGroupListFilter.js
+++ b/guacamole/src/main/webapp/app/groupList/directives/guacGroupListFilter.js
@@ -186,7 +186,7 @@
                 angular.forEach(item.children, function flattenChild(child) {
                     if (child.type === GroupListItem.Type.CONNECTION_GROUP) {
 
-                        var flattenedChild = flattenConnectionGroup(child);
+                        var flattenedChild = flattenGroupListItem(child);
 
                         // Merge all children
                         Array.prototype.push.apply(
diff --git a/guacamole/src/main/webapp/app/groupList/groupListModule.js b/guacamole/src/main/webapp/app/groupList/groupListModule.js
index 14f4706..eb37ac5 100644
--- a/guacamole/src/main/webapp/app/groupList/groupListModule.js
+++ b/guacamole/src/main/webapp/app/groupList/groupListModule.js
@@ -21,4 +21,8 @@
  * Module for displaying the contents of a connection group, allowing the user
  * to select individual connections or groups.
  */
-angular.module('groupList', ['list', 'rest']);
+angular.module('groupList', [
+    'navigation',
+    'list',
+    'rest'
+]);
diff --git a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js
index e662972..29bf91b 100644
--- a/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js
+++ b/guacamole/src/main/webapp/app/groupList/types/GroupListItem.js
@@ -20,7 +20,11 @@
 /**
  * Provides the GroupListItem class definition.
  */
-angular.module('groupList').factory('GroupListItem', ['ConnectionGroup', function defineGroupListItem(ConnectionGroup) {
+angular.module('groupList').factory('GroupListItem', ['$injector', function defineGroupListItem($injector) {
+
+    // Required types
+    var ClientIdentifier = $injector.get('ClientIdentifier');
+    var ConnectionGroup  = $injector.get('ConnectionGroup');
 
     /**
      * Creates a new GroupListItem, initializing the properties of that
@@ -109,15 +113,51 @@
 
         /**
          * Returns the number of currently active users for this connection,
-         * connection group, or sharing profile, if known.
+         * connection group, or sharing profile, if known. If unknown, null may
+         * be returned.
          * 
-         * @type Number
+         * @returns {Number}
+         *     The number of currently active users for this connection,
+         *     connection group, or sharing profile.
          */
         this.getActiveConnections = template.getActiveConnections || (function getActiveConnections() {
             return null;
         });
 
         /**
+         * Returns the unique string identifier that must be used when
+         * connecting to a connection or connection group represented by this
+         * GroupListItem.
+         *
+         * @returns {String}
+         *     The client identifier associated with the connection or
+         *     connection group represented by this GroupListItem, or null if
+         *     this GroupListItem cannot have an associated client identifier.
+         */
+        this.getClientIdentifier = template.getClientIdentifier || function getClientIdentifier() {
+
+            // If the item is a connection, generate a connection identifier
+            if (this.type === GroupListItem.Type.CONNECTION)
+                return ClientIdentifier.toString({
+                    dataSource : this.dataSource,
+                    type       : ClientIdentifier.Types.CONNECTION,
+                    id         : this.identifier
+                });
+
+            // If the item is a connection group, generate a connection group identifier
+            if (this.type === GroupListItem.Type.CONNECTION_GROUP)
+                return ClientIdentifier.toString({
+                    dataSource : this.dataSource,
+                    type       : ClientIdentifier.Types.CONNECTION_GROUP,
+                    id         : this.identifier
+                });
+
+            // Otherwise, no such identifier can exist
+            return null;
+
+        };
+
+        /**
          * The connection, connection group, or sharing profile whose data is
          * exposed within this GroupListItem. If the type of this GroupListItem
          * is not one of the types defined by GroupListItem.Type, then this
diff --git a/guacamole/src/main/webapp/app/home/controllers/homeController.js b/guacamole/src/main/webapp/app/home/controllers/homeController.js
index f0e753d..a6d359e 100644
--- a/guacamole/src/main/webapp/app/home/controllers/homeController.js
+++ b/guacamole/src/main/webapp/app/home/controllers/homeController.js
@@ -25,7 +25,6 @@
 
     // Get required types
     var ConnectionGroup  = $injector.get('ConnectionGroup');
-    var ClientIdentifier = $injector.get('ClientIdentifier');
     var GroupListItem    = $injector.get('GroupListItem');
             
     // Get required services
@@ -70,52 +69,7 @@
      */
     $scope.isLoaded = function isLoaded() {
 
-        return $scope.rootConnectionGroup !== null;
-
-    };
-
-    /**
-     * Object passed to the guacGroupList directive, providing context-specific
-     * functions or data.
-     */
-    $scope.context = {
-
-        /**
-         * Returns the unique string identifier which must be used when
-         * connecting to a connection or connection group represented by the
-         * given GroupListItem.
-         *
-         * @param {GroupListItem} item
-         *     The GroupListItem to determine the client identifier of.
-         *
-         * @returns {String}
-         *     The client identifier associated with the connection or
-         *     connection group represented by the given GroupListItem, or null
-         *     if the GroupListItem cannot have an associated client
-         *     identifier.
-         */
-        getClientIdentifier : function getClientIdentifier(item) {
-
-            // If the item is a connection, generate a connection identifier
-            if (item.type === GroupListItem.Type.CONNECTION)
-                return ClientIdentifier.toString({
-                    dataSource : item.dataSource,
-                    type       : ClientIdentifier.Types.CONNECTION,
-                    id         : item.identifier
-                });
-
-            // If the item is a connection group, generate a connection group identifier
-            if (item.type === GroupListItem.Type.CONNECTION_GROUP)
-                return ClientIdentifier.toString({
-                    dataSource : item.dataSource,
-                    type       : ClientIdentifier.Types.CONNECTION_GROUP,
-                    id         : item.identifier
-                });
-
-            // Otherwise, no such identifier can exist
-            return null;
-
-        }
+        return $scope.rootConnectionGroups !== null;
 
     };
 
@@ -127,6 +81,6 @@
     )
     .then(function rootGroupsRetrieved(rootConnectionGroups) {
         $scope.rootConnectionGroups = rootConnectionGroups;
-    }, requestService.WARN);
+    }, requestService.DIE);
 
 }]);
diff --git a/guacamole/src/main/webapp/app/home/templates/connection.html b/guacamole/src/main/webapp/app/home/templates/connection.html
index 3f244fb..b428412 100644
--- a/guacamole/src/main/webapp/app/home/templates/connection.html
+++ b/guacamole/src/main/webapp/app/home/templates/connection.html
@@ -1,5 +1,5 @@
 <a class="home-connection"
-   ng-href="#/client/{{context.getClientIdentifier(item)}}"
+   ng-href="#/client/{{ item.getClientIdentifier() }}"
    ng-class="{active: item.getActiveConnections()}">
 
     <!-- Connection icon -->
diff --git a/guacamole/src/main/webapp/app/home/templates/connectionGroup.html b/guacamole/src/main/webapp/app/home/templates/connectionGroup.html
index f1261dc..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/{{context.getClientIdentifier(item)}}">{{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/home/templates/home.html b/guacamole/src/main/webapp/app/home/templates/home.html
index f68a0a9..4597dff 100644
--- a/guacamole/src/main/webapp/app/home/templates/home.html
+++ b/guacamole/src/main/webapp/app/home/templates/home.html
@@ -23,7 +23,6 @@
         </div>
         <div class="all-connections">
             <guac-group-list
-                context="context"
                 connection-groups="filteredRootConnectionGroups"
                 templates="{
                     'connection'       : 'app/home/templates/connection.html',
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%
copy from guacamole/src/main/webapp/app/index/config/indexHttpPatchConfig.js
copy 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/controllers/indexController.js b/guacamole/src/main/webapp/app/index/controllers/indexController.js
index 5cff962..15e7819 100644
--- a/guacamole/src/main/webapp/app/index/controllers/indexController.js
+++ b/guacamole/src/main/webapp/app/index/controllers/indexController.js
@@ -28,7 +28,15 @@
     var $window          = $injector.get('$window');
     var clipboardService = $injector.get('clipboardService');
     var guacNotification = $injector.get('guacNotification');
-    
+
+    /**
+     * The error that prevents the current page from rendering at all. If no
+     * such error has occurred, this will be null.
+     *
+     * @type Error
+     */
+    $scope.fatalError = null;
+
     /**
      * The notification service.
      */
@@ -159,6 +167,7 @@
         $scope.loginHelpText = null;
         $scope.acceptedCredentials = {};
         $scope.expectedCredentials = error.expected;
+        $scope.fatalError = null;
     });
 
     // Prompt for remaining credentials if provided credentials were not enough
@@ -168,6 +177,15 @@
         $scope.loginHelpText = error.translatableMessage;
         $scope.acceptedCredentials = parameters;
         $scope.expectedCredentials = error.expected;
+        $scope.fatalError = null;
+    });
+
+    // Replace absolutely all content with an error message if the page itself
+    // cannot be displayed due to an error
+    $scope.$on('guacFatalPageError', function fatalPageError(error) {
+        $scope.page.title = 'APP.NAME';
+        $scope.page.bodyClassName = '';
+        $scope.fatalError = error;
     });
 
     // Update title and CSS class upon navigation
@@ -181,6 +199,7 @@
             $scope.loginHelpText = null;
             $scope.acceptedCredentials = null;
             $scope.expectedCredentials = null;
+            $scope.fatalError = null;
 
             // Set title
             var title = current.$$route.title;
diff --git a/guacamole/src/main/webapp/app/index/styles/fatal-page-error.css b/guacamole/src/main/webapp/app/index/styles/fatal-page-error.css
new file mode 100644
index 0000000..9a50e9c
--- /dev/null
+++ b/guacamole/src/main/webapp/app/index/styles/fatal-page-error.css
@@ -0,0 +1,76 @@
+/*
+ * 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.
+ */
+
+.fatal-page-error-outer {
+    display: table;
+    height: 100%;
+    width: 100%;
+    position: fixed;
+    left: 0;
+    top: 0;
+    z-index: 30;
+}
+
+.fatal-page-error-middle {
+    width: 100%;
+    text-align: center;
+    display: table-cell;
+    vertical-align: middle;
+}
+
+.fatal-page-error {
+    display: inline-block;
+    width: 100%;
+    max-width: 5in;
+    padding: 1em;
+    text-align: left;
+}
+
+.fatal-page-error h1 {
+    text-transform: uppercase;
+    padding: 0;
+    padding-right: 1em;
+}
+
+.fatal-page-error h1::before {
+    content: ' ';
+    display: inline-block;
+    background: url('images/warning.png');
+    background-repeat: no-repeat;
+    height: 1em;
+    width: 1em;
+    background-size: contain;
+    margin: 0 0.25em;
+    margin-bottom: -0.2em;
+}
+
+/* Ensure fatal error is initially hidden, fading the error message in when
+ * needed */
+
+.fatal-page-error-outer {
+    visibility: hidden;
+    opacity: 0;
+    transition: opacity, visibility;
+    transition-duration: 0.25s;
+}
+
+.shown.fatal-page-error-outer {
+    visibility: visible;
+    opacity: 1;
+}
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/controllers/manageConnectionController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js
index b84886f..06cbeae 100644
--- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js
+++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionController.js
@@ -287,7 +287,7 @@
                     PermissionSet.hasConnectionPermission,
                     identifier);
 
-    }, requestService.WARN);
+    }, requestService.DIE);
     
     // Get history date format
     $translate('MANAGE_CONNECTION.FORMAT_HISTORY_START').then(function historyDateFormatReceived(historyDateFormat) {
diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js
index 250ebc1..1d81773 100644
--- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js
+++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js
@@ -245,7 +245,7 @@
                     PermissionSet.hasConnectionPermission,
                     identifier);
 
-    }, requestService.WARN);
+    }, requestService.DIE);
 
     /**
      * Cancels all pending edits, returning to the main list of connections
diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js
index 61bfbe9..e3c9ca1 100644
--- a/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js
+++ b/guacamole/src/main/webapp/app/manage/controllers/manageSharingProfileController.js
@@ -277,7 +277,7 @@
                     PermissionSet.hasConnectionPermission,
                     identifier);
 
-    }, requestService.WARN);
+    }, requestService.DIE);
 
     /**
      * @borrows Protocol.getNamespace
diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
index 42568a7..f7ead13 100644
--- a/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
+++ b/guacamole/src/main/webapp/app/manage/controllers/manageUserController.js
@@ -399,7 +399,7 @@
 
         });
 
-    }, requestService.WARN);
+    }, requestService.DIE);
 
     /**
      * Returns the URL for the page which manages the user account currently
diff --git a/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js b/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js
index f47670a..9392bb0 100644
--- a/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js
+++ b/guacamole/src/main/webapp/app/manage/directives/connectionPermissionEditor.js
@@ -348,7 +348,7 @@
 
             });
 
-        }, requestService.WARN);
+        }, requestService.DIE);
 
         /**
          * Updates the permissionsAdded and permissionsRemoved permission sets
diff --git a/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js b/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js
index 67fd3f4..78377a7 100644
--- a/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js
+++ b/guacamole/src/main/webapp/app/manage/directives/systemPermissionEditor.js
@@ -151,7 +151,7 @@
         )
         .then(function permissionsReceived(permissions) {
             $scope.permissions = permissions;
-        }, requestService.WARN);
+        }, requestService.DIE);
 
         /**
          * Returns whether the current user has permission to change the system
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 e00cc1c..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 : '='
 
         },
 
@@ -53,6 +62,14 @@
             var element = $element[0];
 
             /**
+             * The element containing the menu contents that display when the
+             * menu is open.
+             *
+             * @type Element
+             */
+            var contents = $element.find('.menu-contents')[0];
+
+            /**
              * The main document object.
              *
              * @type Document
@@ -73,16 +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();
+            // Prevent clicks within menu contents from toggling menu visibility
+            // if the menu contents are intended to be interactive
+            contents.addEventListener('click', function clickInsideMenuContents(e) {
+                if ($scope.interactive)
+                    e.stopPropagation();
             }, false);
 
         }] // end controller
diff --git a/guacamole/src/main/webapp/app/navigation/directives/guacUserMenu.js b/guacamole/src/main/webapp/app/navigation/directives/guacUserMenu.js
index 492a867..c6630e7 100644
--- a/guacamole/src/main/webapp/app/navigation/directives/guacUserMenu.js
+++ b/guacamole/src/main/webapp/app/navigation/directives/guacUserMenu.js
@@ -95,13 +95,10 @@
              */
             $scope.role = null;
 
-            // Pull user data
+            // Display user profile attributes if available
             userService.getUser(authenticationService.getDataSource(), $scope.username)
                     .then(function userRetrieved(user) {
 
-                // Store retrieved user object
-                $scope.user = user;
-
                 // Pull basic profile information
                 $scope.fullName = user.attributes[User.Attributes.FULL_NAME];
                 $scope.organization = user.attributes[User.Attributes.ORGANIZATION];
@@ -111,7 +108,7 @@
                 var email = user.attributes[User.Attributes.EMAIL_ADDRESS];
                 $scope.userURL = email ? 'mailto:' + email : null;
 
-            }, requestService.WARN);
+            }, requestService.IGNORE);
 
             /**
              * The available main pages for the current user.
diff --git a/guacamole/src/main/webapp/app/navigation/services/userPageService.js b/guacamole/src/main/webapp/app/navigation/services/userPageService.js
index f5bc308..f91303a 100644
--- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js
+++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js
@@ -67,77 +67,98 @@
      */
     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
         // evaluation for automatic connections.  The Preferences page is
-        // a Settings page and is always visible, so we look for more than
-        // one to indicate access to administrative pages.
-        if (settingsPages.length > 1)
+        // a Settings page and is always visible, and the Session management
+        // page is also available to all users so that they can kill their
+        // own session.  We look for more than those two pages to determine
+        // if we should go to the home page.
+        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;
 
     };
 
@@ -170,7 +191,7 @@
         })
         .then(function rootConnectionGroupsPermissionsRetrieved(data) {
             deferred.resolve(generateHomePage(data.rootGroups,data.permissionsSets));
-        }, requestService.WARN);
+        }, requestService.DIE);
 
         return deferred.promise;
 
@@ -195,7 +216,6 @@
         var canManageUserGroups = [];
         var canManageConnections = [];
         var canViewConnectionRecords = [];
-        var canManageSessions = [];
 
         // Inspect the contents of each provided permission set
         angular.forEach(authenticationService.getAvailableDataSources(), function inspectPermissions(dataSource) {
@@ -276,24 +296,21 @@
                 canManageConnections.push(dataSource);
             }
 
-            // Determine whether the current user needs access to the session management UI or view connection history
+            // Determine whether the current user needs access to view connection history
             if (
-                    // A user must be a system administrator to manage sessions
+                    // A user must be a system administrator to view connection records
                     PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER)
             ) {
-                canManageSessions.push(dataSource);
                 canViewConnectionRecords.push(dataSource);
             }
 
         });
 
-        // If user can manage sessions, add link to sessions management page
-        if (canManageSessions.length) {
-            pages.push(new PageDefinition({
-                name : 'USER_MENU.ACTION_MANAGE_SESSIONS',
-                url  : '/settings/sessions'
-            }));
-        }
+        // Add link to Session management (always accessible)
+        pages.push(new PageDefinition({
+            name : 'USER_MENU.ACTION_MANAGE_SESSIONS',
+            url  : '/settings/sessions'
+        }));
 
         // If user can manage connections, add links for connection management pages
         angular.forEach(canViewConnectionRecords, function addConnectionHistoryLink(dataSource) {
@@ -365,7 +382,7 @@
         // Resolve promise using settings pages derived from permissions
         .then(function permissionsRetrieved(permissions) {
             deferred.resolve(generateSettingsPages(permissions));
-        }, requestService.WARN);
+        }, requestService.DIE);
         
         return deferred.promise;
 
@@ -446,7 +463,7 @@
         .then(function rootConnectionGroupsRetrieved(retrievedRootGroups) {
             rootGroups = retrievedRootGroups;
             resolveMainPages();
-        }, requestService.WARN);
+        }, requestService.DIE);
 
         // Retrieve current permissions
         dataSourceService.apply(
@@ -459,7 +476,7 @@
         .then(function permissionsRetrieved(retrievedPermissions) {
             permissions = retrievedPermissions;
             resolveMainPages();
-        }, requestService.WARN);
+        }, requestService.DIE);
         
         return deferred.promise;
 
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/services/requestService.js b/guacamole/src/main/webapp/app/rest/services/requestService.js
index 8d20976..0b135e7 100644
--- a/guacamole/src/main/webapp/app/rest/services/requestService.js
+++ b/guacamole/src/main/webapp/app/rest/services/requestService.js
@@ -25,8 +25,9 @@
         function requestService($injector) {
 
     // Required services
-    var $http = $injector.get('$http');
-    var $log  = $injector.get('$log');
+    var $http      = $injector.get('$http');
+    var $log       = $injector.get('$log');
+    var $rootScope = $injector.get('$rootScope');
 
     // Required types
     var Error = $injector.get('Error');
@@ -142,6 +143,21 @@
         $log.warn(error.type, error.message || error.translatableMessage);
     });
 
+    /**
+     * Promise error callback which replaces the content of the page with a
+     * generic error message warning that the page could not be displayed. All
+     * rejections are logged to the browser console as errors. This callback
+     * should be used in favor of @link{WARN} if REST errors will result in the
+     * page being unusable.
+     *
+     * @constant
+     * @type Function
+     */
+    service.DIE = service.createErrorCallback(function fatalPageError(error) {
+        $rootScope.$broadcast('guacFatalPageError', error);
+        $log.error(error.type, error.message || error.translatableMessage);
+    });
+
     return service;
 
 }]);
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/guacSettingsConnectionHistory.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnectionHistory.js
index c184a13..796edcd 100644
--- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnectionHistory.js
+++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnectionHistory.js
@@ -178,7 +178,7 @@
                        $scope.historyEntryWrappers.push(new ConnectionHistoryEntryWrapper(historyEntry)); 
                     });
 
-                }, requestService.WARN);
+                }, requestService.DIE);
 
             };
             
diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js
index 2f7fafb..05c86ef 100644
--- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js
+++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsConnections.js
@@ -415,9 +415,9 @@
                 )
                 .then(function connectionGroupsReceived(rootGroups) {
                     $scope.rootGroups = rootGroups;
-                }, requestService.WARN);
+                }, requestService.DIE);
 
-            }, requestService.WARN); // end retrieve permissions
+            }, requestService.DIE); // end retrieve permissions
 
         }]
     };
diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js
index dfad564..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.WARN);
-
             // 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 67776f0..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');
 
@@ -189,12 +190,14 @@
                         var connection = allConnections[dataSource][activeConnection.connectionIdentifier];
 
                         // Add wrapper
-                        $scope.wrappers.push(new ActiveConnectionWrapper({
-                            dataSource       : dataSource,
-                            name             : connection.name,
-                            startDate        : $filter('date')(activeConnection.startDate, sessionDateFormat),
-                            activeConnection : activeConnection
-                        }));
+                        if (activeConnection.username !== null) {
+                            $scope.wrappers.push(new ActiveConnectionWrapper({
+                                dataSource       : dataSource,
+                                name             : connection.name,
+                                startDate        : $filter('date')(activeConnection.startDate, sessionDateFormat),
+                                activeConnection : activeConnection
+                            }));
+                        }
 
                     });
                 });
@@ -220,7 +223,7 @@
                 // Attempt to produce wrapped list of active connections
                 wrapAllActiveConnections();
 
-            }, requestService.WARN);
+            }, requestService.DIE);
             
             // Query active sessions
             dataSourceService.apply(
@@ -235,7 +238,7 @@
                 // Attempt to produce wrapped list of active connections
                 wrapAllActiveConnections();
 
-            }, requestService.WARN);
+            }, requestService.DIE);
 
             // Get session date format
             $translate('SETTINGS_SESSIONS.FORMAT_STARTDATE').then(function sessionDateFormatReceived(retrievedSessionDateFormat) {
@@ -334,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/directives/guacSettingsUsers.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js
index 4adf04e..6b8c6e0 100644
--- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js
+++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsUsers.js
@@ -88,6 +88,8 @@
              * @type String[]
              */
             $scope.filteredUserProperties = [
+                'user.attributes["guac-full-name"]',
+                'user.attributes["guac-organization"]',
                 'user.lastActive',
                 'user.username'
             ];
@@ -107,7 +109,9 @@
              */
             $scope.order = new SortOrder([
                 'user.username',
-                '-user.lastActive'
+                '-user.lastActive',
+                'user.attributes["guac-organization"]',
+                'user.attributes["guac-full-name"]'
             ]);
 
             // Get session date format
@@ -277,9 +281,9 @@
                         });
                     });
 
-                }, requestService.WARN);
+                }, requestService.DIE);
 
-            }, requestService.WARN);
+            }, requestService.DIE);
             
         }]
     };
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/styles/user-list.css b/guacamole/src/main/webapp/app/settings/styles/user-list.css
index 6c0edd5..e130d0d 100644
--- a/guacamole/src/main/webapp/app/settings/styles/user-list.css
+++ b/guacamole/src/main/webapp/app/settings/styles/user-list.css
@@ -24,12 +24,11 @@
 .settings.users table.user-list th.last-active,
 .settings.users table.user-list td.last-active {
     white-space: nowrap;
-    width: 0;
 }
 
-.settings.users table.user-list th.username,
-.settings.users table.user-list td.username {
-    width: 100%;
+.settings.users table.user-list th,
+.settings.users table.user-list td {
+    width: 25%;
 }
 
 .settings.users table.user-list tr.user td.username a[href] {
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/app/settings/templates/settingsUsers.html b/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html
index 67f6760..232a1f2 100644
--- a/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html
+++ b/guacamole/src/main/webapp/app/settings/templates/settingsUsers.html
@@ -27,6 +27,12 @@
                 <th guac-sort-order="order" guac-sort-property="'user.username'" class="username">
                     {{'SETTINGS_USERS.TABLE_HEADER_USERNAME' | translate}}
                 </th>
+                <th guac-sort-order="order" guac-sort-property="'user.attributes[\'guac-organization\']'" class="organization">
+                    {{'SETTINGS_USERS.TABLE_HEADER_ORGANIZATION' | translate}}
+                </th>
+                <th guac-sort-order="order" guac-sort-property="'user.attributes[\'guac-full-name\']'" class="full-name">
+                    {{'SETTINGS_USERS.TABLE_HEADER_FULL_NAME' | translate}}
+                </th>
                 <th guac-sort-order="order" guac-sort-property="'user.lastActive'" class="last-active">
                     {{'SETTINGS_USERS.TABLE_HEADER_LAST_ACTIVE' | translate}}
                 </th>
@@ -40,6 +46,8 @@
                         <span class="name">{{manageableUser.user.username}}</span>
                     </a>
                 </td>
+                <td class="organization">{{manageableUser.user.attributes['guac-organization']}}</td>
+                <td class="full-name">{{manageableUser.user.attributes['guac-full-name']}}</td>
                 <td class="last-active">{{manageableUser.user.lastActive | date : dateFormat}}</td>
             </tr>
         </tbody>
diff --git a/guacamole/src/main/webapp/images/arrows/left.png b/guacamole/src/main/webapp/images/arrows/left.png
new file mode 100644
index 0000000..920a4fd
--- /dev/null
+++ b/guacamole/src/main/webapp/images/arrows/left.png
Binary files differ
diff --git a/guacamole/src/main/webapp/images/warning-white.png b/guacamole/src/main/webapp/images/warning-white.png
new file mode 100644
index 0000000..7254ea7
--- /dev/null
+++ b/guacamole/src/main/webapp/images/warning-white.png
Binary files differ
diff --git a/guacamole/src/main/webapp/index.html b/guacamole/src/main/webapp/index.html
index a4a93da..309f114 100644
--- a/guacamole/src/main/webapp/index.html
+++ b/guacamole/src/main/webapp/index.html
@@ -27,31 +27,46 @@
         <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>
     <body ng-class="page.bodyClassName">
 
-        <!-- Content for logged-in users -->
-        <div ng-if="!expectedCredentials">
-        
-            <!-- Global status/error dialog -->
-            <div ng-class="{shown: guacNotification.getStatus()}" class="status-outer">
-                <div class="status-middle">
-                    <guac-notification notification="guacNotification.getStatus()"></guac-notification>
+        <div ng-if="!fatalError">
+
+            <!-- Content for logged-in users -->
+            <div ng-if="!expectedCredentials">
+
+                <!-- Global status/error dialog -->
+                <div ng-class="{shown: guacNotification.getStatus()}" class="status-outer">
+                    <div class="status-middle">
+                        <guac-notification notification="guacNotification.getStatus()"></guac-notification>
+                    </div>
                 </div>
+
+                <div id="content" ng-view>
+                </div>
+
             </div>
-            
-            <div id="content" ng-view>
-            </div>
-            
+
+            <!-- Login screen for logged-out users -->
+            <guac-login ng-show="expectedCredentials"
+                        help-text="loginHelpText"
+                        form="expectedCredentials"
+                        values="acceptedCredentials"></guac-login>
+
         </div>
 
-        <!-- Login screen for logged-out users -->
-        <guac-login ng-show="expectedCredentials"
-                    help-text="loginHelpText"
-                    form="expectedCredentials"
-                    values="acceptedCredentials"></guac-login>
+        <!-- Absolute fatal error -->
+        <div ng-if="fatalError" ng-class="{shown: fatalError}" class="fatal-page-error-outer">
+            <div class="fatal-page-error-middle">
+                <div class="fatal-page-error">
+                    <h1 translate="APP.DIALOG_HEADER_ERROR"></h1>
+                    <p translate="APP.ERROR_PAGE_UNAVAILABLE"></p>
+                </div>
+            </div>
+        </div>
 
         <!-- Reformat URL for AngularJS if query parameters are present -->
         <script type="text/javascript" src="relocateParameters.js"></script>
@@ -71,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/layouts/es-es-qwerty.json b/guacamole/src/main/webapp/layouts/es-es-qwerty.json
index 0a1e7d7..dc330e5 100644
--- a/guacamole/src/main/webapp/layouts/es-es-qwerty.json
+++ b/guacamole/src/main/webapp/layouts/es-es-qwerty.json
@@ -1,4 +1,4 @@
-{

+{

 

     "language" : "es_ES",

     "type"     : "qwerty",

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/de.json b/guacamole/src/main/webapp/translations/de.json
index 90953e5..6f73f5c 100644
--- a/guacamole/src/main/webapp/translations/de.json
+++ b/guacamole/src/main/webapp/translations/de.json
@@ -91,6 +91,8 @@
         "ERROR_UPLOAD_31D"     : "Die maximale Anzahl gleichzeiter Dateiübertragungen erreicht. Bitte warte bis laufende Dateiübertagungen abgeschlossen sind und versuche es erneut.",
         "ERROR_UPLOAD_DEFAULT" : "Die Verbindung wurde aufgrund eines interen Fehlers im Guacamole Server beendet. Sollte dieses Problem weiterhin bestehen informieren Sie den Systemadministrator oder überprüfen Sie die Protokolle.",
 
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
         "HELP_CLIPBOARD"           : "Kopierter oder ausgeschnittener Text aus Guacamole wird hier angezeigt. Änderungen am Text werden direkt auf die entfernte Zwischenablage angewandt.",
         "HELP_INPUT_METHOD_NONE"   : "Keine Eingabemethode in Verwendung. Tastatureingaben werden von der Hardwaretastatur akzeptiert.",
         "HELP_INPUT_METHOD_OSK"    : "Bildschirmeingaben und die eingebettete Guacamole Bildschrimtastatur werden akzeptiert. Die Bildschirmtastatur gestattet Tastenkombinationen die ansonsten unmöglich sind (z.B.: Strg-Alt-Del).",
@@ -354,6 +356,7 @@
 
         "SECTION_HEADER_AUTHENTICATION"     : "Authentifizierung",
         "SECTION_HEADER_BASIC_PARAMETERS"   : "Basiseinstellungen",
+        "SECTION_HEADER_CLIPBOARD"          : "Zwischenablage",
         "SECTION_HEADER_DEVICE_REDIRECTION" : "Geräteumleitung",
         "SECTION_HEADER_DISPLAY"            : "Bildschirm",
         "SECTION_HEADER_NETWORK"            : "Netzwerk",
@@ -403,6 +406,7 @@
         "NAME" : "SSH",
 
         "SECTION_HEADER_AUTHENTICATION" : "Authentifizierung",
+        "SECTION_HEADER_CLIPBOARD"      : "Zwischenablage",
         "SECTION_HEADER_DISPLAY"        : "Bildschirm",
         "SECTION_HEADER_NETWORK"        : "Netzwerk",
         "SECTION_HEADER_SESSION"        : "Sitzung / Umgebung",
@@ -447,6 +451,7 @@
         "NAME" : "Telnet",
 
         "SECTION_HEADER_AUTHENTICATION" : "Authentifizierung",
+        "SECTION_HEADER_CLIPBOARD"      : "Zwischenablage",
         "SECTION_HEADER_DISPLAY"        : "Bildschirm",
         "SECTION_HEADER_NETWORK"        : "Netzwerk"
 
@@ -621,7 +626,7 @@
         
         "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
 
-        "HELP_SESSIONS" : "Alle aktiven Guacamole Sitzungen werden hier aufgelistet. Wenn Sie eine oder mehrere Sitzungen beenden wollen, wählen Sie diese Sitzung durch Aktivierung der nebenstehende Box und klicken auf \"Beende Sitzung\". Beendung einer Sitzung trennt den Benutzer von dessen Verbindung unverzüglich.",
+        "HELP_SESSIONS" : "Diese Seite wird mit derzeit aktiven Verbindungen gefüllt. Die aufgelisteten Verbindungen und die Möglichkeit, diese Verbindungen zu beenden, hängen von Ihrer Zugriffsebene ab. Wenn Sie eine oder mehrere Sitzungen beenden wollen, wählen Sie diese Sitzung durch Aktivierung der nebenstehende Box und klicken auf \"Beende Sitzung\". Beendung einer Sitzung trennt den Benutzer von dessen Verbindung unverzüglich.",
         
         "INFO_NO_SESSIONS" : "Keine aktiven Sitzungen",
 
diff --git a/guacamole/src/main/webapp/translations/en.json b/guacamole/src/main/webapp/translations/en.json
index 24ab0d7..4e6420c 100644
--- a/guacamole/src/main/webapp/translations/en.json
+++ b/guacamole/src/main/webapp/translations/en.json
@@ -32,6 +32,7 @@
 
         "DIALOG_HEADER_ERROR" : "Error",
 
+        "ERROR_PAGE_UNAVAILABLE"  : "An error has occurred and this action cannot be completed. If the problem persists, please notify your system administrator or check your system logs.",
         "ERROR_PASSWORD_BLANK"    : "Your password cannot be blank.",
         "ERROR_PASSWORD_MISMATCH" : "The provided passwords do not match.",
         
@@ -95,7 +96,7 @@
 
         "ERROR_UPLOAD_100"     : "File transfer is either not supported or not enabled. Please contact your system administrator, or check your system logs.",
         "ERROR_UPLOAD_201"     : "Too many files are currently being transferred. Please wait for existing transfers to complete, and then try again.",
-        "ERROR_UPLOAD_202"     : "The file cannot be transferred because the remote desktop server is taking too long to respond. Please try again or or contact your system administrator.",
+        "ERROR_UPLOAD_202"     : "The file cannot be transferred because the remote desktop server is taking too long to respond. Please try again or contact your system administrator.",
         "ERROR_UPLOAD_203"     : "The remote desktop server encountered an error during transfer. Please try again or contact your system administrator.",
         "ERROR_UPLOAD_204"     : "The destination for the file transfer does not exist. Please check that the destination exists and try again.",
         "ERROR_UPLOAD_205"     : "The destination for the file transfer is currently locked. Please wait for any in-progress tasks to complete and try again.",
@@ -105,6 +106,8 @@
         "ERROR_UPLOAD_31D"     : "Too many files are currently being transferred. Please wait for existing transfers to complete, and then try again.",
         "ERROR_UPLOAD_DEFAULT" : "An internal error has occurred within the Guacamole server, and the connection has been terminated. If the problem persists, please notify your system administrator, or check your system logs.",
 
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
         "HELP_CLIPBOARD"           : "Text copied/cut within Guacamole will appear here. Changes to the text below will affect the remote clipboard.",
         "HELP_INPUT_METHOD_NONE"   : "No input method is used. Keyboard input is accepted from a connected, physical keyboard.",
         "HELP_INPUT_METHOD_OSK"    : "Display and accept input from the built-in Guacamole on-screen keyboard. The on-screen keyboard allows typing of key combinations that may otherwise be impossible (such as Ctrl-Alt-Del).",
@@ -147,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)"
     },
@@ -361,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:",
@@ -372,6 +457,8 @@
         "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automatically create recording path:",
         "FIELD_HEADER_DISABLE_AUDIO"   : "Disable audio:",
         "FIELD_HEADER_DISABLE_AUTH"    : "Disable authentication:",
+        "FIELD_HEADER_DISABLE_COPY"    : "Disable copying from remote desktop:",
+        "FIELD_HEADER_DISABLE_PASTE"   : "Disable pasting from client:",
         "FIELD_HEADER_DOMAIN"          : "Domain:",
         "FIELD_HEADER_DPI"             : "Resolution (DPI):",
         "FIELD_HEADER_DRIVE_NAME"      : "Drive name:",
@@ -427,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:",
 
@@ -446,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)",
@@ -454,16 +543,19 @@
         "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)",
         "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Swedish (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_DA_DK_QWERTY" : "Danish (Qwerty)",
         "FIELD_OPTION_SERVER_LAYOUT_TR_TR_QWERTY" : "Turkish-Q (Qwerty)",
 
         "NAME" : "RDP",
 
         "SECTION_HEADER_AUTHENTICATION"     : "Authentication",
         "SECTION_HEADER_BASIC_PARAMETERS"   : "Basic Settings",
+        "SECTION_HEADER_CLIPBOARD"          : "Clipboard",
         "SECTION_HEADER_DEVICE_REDIRECTION" : "Device Redirection",
         "SECTION_HEADER_DISPLAY"            : "Display",
         "SECTION_HEADER_GATEWAY"            : "Remote Desktop Gateway",
@@ -484,16 +576,20 @@
         "FIELD_HEADER_COMMAND"      : "Execute command:",
         "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automatically create recording path:",
         "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automatically create typescript path:",
+        "FIELD_HEADER_DISABLE_COPY"  : "Disable copying from terminal:",
+        "FIELD_HEADER_DISABLE_PASTE" : "Disable pasting from client:",
         "FIELD_HEADER_FONT_NAME"     : "Font name:",
         "FIELD_HEADER_FONT_SIZE"     : "Font size:",
         "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:",
@@ -503,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:",
 
@@ -544,6 +641,7 @@
 
         "SECTION_HEADER_AUTHENTICATION" : "Authentication",
         "SECTION_HEADER_BEHAVIOR"       : "Terminal behavior",
+        "SECTION_HEADER_CLIPBOARD"      : "Clipboard",
         "SECTION_HEADER_DISPLAY"        : "Display",
         "SECTION_HEADER_NETWORK"        : "Network",
         "SECTION_HEADER_RECORDING"      : "Screen Recording",
@@ -559,10 +657,15 @@
         "FIELD_HEADER_COLOR_SCHEME"   : "Color scheme:",
         "FIELD_HEADER_CREATE_RECORDING_PATH" : "Automatically create recording path:",
         "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "Automatically create typescript path:",
+        "FIELD_HEADER_DISABLE_COPY"   : "Disable copying from terminal:",
+        "FIELD_HEADER_DISABLE_PASTE"  : "Disable pasting from client:",
         "FIELD_HEADER_FONT_NAME"      : "Font name:",
         "FIELD_HEADER_FONT_SIZE"      : "Font size:",
         "FIELD_HEADER_HOSTNAME"       : "Hostname:",
+        "FIELD_HEADER_LOGIN_FAILURE_REGEX" : "Login failure regular expression:",
+        "FIELD_HEADER_LOGIN_SUCCESS_REGEX" : "Login success regular expression:",
         "FIELD_HEADER_USERNAME"       : "Username:",
+        "FIELD_HEADER_USERNAME_REGEX" : "Username regular expression:",
         "FIELD_HEADER_PASSWORD"       : "Password:",
         "FIELD_HEADER_PASSWORD_REGEX" : "Password regular expression:",
         "FIELD_HEADER_PORT"           : "Port:",
@@ -572,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:",
@@ -614,6 +718,7 @@
 
         "SECTION_HEADER_AUTHENTICATION" : "Authentication",
         "SECTION_HEADER_BEHAVIOR"       : "Terminal behavior",
+        "SECTION_HEADER_CLIPBOARD"      : "Clipboard",
         "SECTION_HEADER_DISPLAY"        : "Display",
         "SECTION_HEADER_RECORDING"      : "Screen Recording",
         "SECTION_HEADER_TYPESCRIPT"     : "Typescript (Text Session Recording)",
@@ -630,6 +735,8 @@
         "FIELD_HEADER_CURSOR"           : "Cursor:",
         "FIELD_HEADER_DEST_HOST"        : "Destination host:",
         "FIELD_HEADER_DEST_PORT"        : "Destination port:",
+        "FIELD_HEADER_DISABLE_COPY"     : "Disable copying from remote desktop:",
+        "FIELD_HEADER_DISABLE_PASTE"    : "Disable pasting from client:",
         "FIELD_HEADER_ENABLE_AUDIO"     : "Enable audio:",
         "FIELD_HEADER_ENABLE_SFTP"      : "Enable SFTP:",
         "FIELD_HEADER_HOSTNAME"         : "Hostname:",
@@ -749,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.",
@@ -756,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.",
@@ -788,7 +896,9 @@
 
         "SECTION_HEADER_USERS"       : "Users",
 
+        "TABLE_HEADER_FULL_NAME"   : "Full name",
         "TABLE_HEADER_LAST_ACTIVE" : "Last active",
+        "TABLE_HEADER_ORGANIZATION" : "Organization",
         "TABLE_HEADER_USERNAME"    : "Username"
 
     },
@@ -825,7 +935,7 @@
         
         "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
 
-        "HELP_SESSIONS" : "All currently-active Guacamole sessions are listed here. If you wish to kill one or more sessions, check the box next to those sessions and click \"Kill Sessions\". Killing a session will immediately disconnect the user from the associated connection.",
+        "HELP_SESSIONS" : "This page will be populated with currently-active connections. The connections listed and the ability to kill those connections is dependent upon your access level. If you wish to kill one or more sessions, check the box next to those sessions and click \"Kill Sessions\". Killing a session will immediately disconnect the user from the associated connection.",
         
         "INFO_NO_SESSIONS" : "No active sessions",
 
diff --git a/guacamole/src/main/webapp/translations/es.json b/guacamole/src/main/webapp/translations/es.json
index 283779a..903c642 100644
--- a/guacamole/src/main/webapp/translations/es.json
+++ b/guacamole/src/main/webapp/translations/es.json
@@ -4,9 +4,6 @@
     
     "APP" : {
 
-        "NAME"    : "Apache Guacamole",
-        "VERSION" : "0.9.13-incubating",
-
         "ACTION_ACKNOWLEDGE"        : "OK",
         "ACTION_CANCEL"             : "Cancelar",
         "ACTION_CLONE"              : "Clonar",
@@ -104,6 +101,8 @@
         "ERROR_UPLOAD_31D"     : "Se estan transfiriendo muchos ficheros actualmente. Por favor espere a que finalicen las transferencias de fichero existentes e intente de nuevo.",
         "ERROR_UPLOAD_DEFAULT" : "Ha ocurrido un error interno en el servidor Guacamole y la conexión ha finalizado. Si el problema persiste, por favor notifíquelo al administrador o compruebe los registros del sistema.",
 
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
         "HELP_CLIPBOARD"           : "Aquí aparecerá el texto copiado/cortado en Guacamole. Los cambios en el texto de abajo afectaran al portapapeles remoto.",
         "HELP_INPUT_METHOD_NONE"   : "No se está usando un método de entrada. La entrada de teclado se acepta desde un teclado físico conectado.",
         "HELP_INPUT_METHOD_OSK"    : "Muestra y acepta entrada desde el teclado en pantalla incorporado de Guacamole. El teclado en pantalla permite escribir combinaciones que serían imposible de otro modo (como Ctrl-Alt-Sup).",
@@ -399,6 +398,7 @@
 
         "SECTION_HEADER_AUTHENTICATION"     : "Autenticación",
         "SECTION_HEADER_BASIC_PARAMETERS"   : "Configuración básica",
+        "SECTION_HEADER_CLIPBOARD"          : "Portapapeles",
         "SECTION_HEADER_DEVICE_REDIRECTION" : "Redirección dispositivo",
         "SECTION_HEADER_DISPLAY"            : "Visualización",
         "SECTION_HEADER_GATEWAY"            : "Puerta de enlace remota",
@@ -460,6 +460,7 @@
         "NAME" : "SSH",
 
         "SECTION_HEADER_AUTHENTICATION" : "Autenticación",
+        "SECTION_HEADER_CLIPBOARD"      : "Portapapeles",
         "SECTION_HEADER_DISPLAY"        : "Mostrar",
         "SECTION_HEADER_NETWORK"        : "Red",
         "SECTION_HEADER_RECORDING"      : "Grabación de pantalla",
@@ -512,6 +513,7 @@
         "NAME" : "Telnet",
 
         "SECTION_HEADER_AUTHENTICATION" : "Autenticación",
+        "SECTION_HEADER_CLIPBOARD"      : "Portapapeles",
         "SECTION_HEADER_DISPLAY"        : "Mostrar",
         "SECTION_HEADER_RECORDING"      : "Grabación pantalla",
         "SECTION_HEADER_TYPESCRIPT"     : "Script de escritura (Próxima sesión de grabación)",
@@ -678,7 +680,11 @@
 
         "HELP_USERS" : "Haga Clic o toque un usuario abajo para gestionar dicho usuario. Dependiendo de su nivel de acceso, podrá añadir/borrar usuarios y cambiar sus contraseñas.",
 
-        "SECTION_HEADER_USERS"       : "Usuarios"
+        "SECTION_HEADER_USERS"       : "Usuarios",
+
+        "TABLE_HEADER_FULL_NAME"   : "Nombre completo",
+        "TABLE_HEADER_ORGANIZATION" : "Organización",
+        "TABLE_HEADER_USERNAME"    : "Usuario"
 
     },
     
@@ -695,7 +701,7 @@
         
         "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
 
-        "HELP_SESSIONS" : "Aquí se listan todas las sesiones activas que tiene actualmente Guacamole. Si quiere finalizar una o mas sesiones, marque la casilla correspondiente a esa/s sesión/es y haga clic en \"Finalizar Sesiones\". Si finaliza una sesión desconectará inmediatamente al usuario de la conexión asociada.",
+        "HELP_SESSIONS" : "Esta página se completará con las conexiones actualmente activas. Las conexiones enumeradas y la capacidad de eliminar esas conexiones dependen de su nivel de acceso. Si quiere finalizar una o mas sesiones, marque la casilla correspondiente a esa/s sesión/es y haga clic en \"Finalizar Sesiones\". Si finaliza una sesión desconectará inmediatamente al usuario de la conexión asociada.",
         
         "INFO_NO_SESSIONS" : "No hay sesiones activas",
 
diff --git a/guacamole/src/main/webapp/translations/fr.json b/guacamole/src/main/webapp/translations/fr.json
index 8ebbd36..00cee95 100644
--- a/guacamole/src/main/webapp/translations/fr.json
+++ b/guacamole/src/main/webapp/translations/fr.json
@@ -91,6 +91,8 @@
         "ERROR_UPLOAD_31D"     : "Trop de fichiers sont actuellement transférés. Merci d'attendre que les transferts en cours soient terminés et de réessayer plus tard.", 
         "ERROR_UPLOAD_DEFAULT" : "Une erreur interne est apparue dans le serveur Guacamole et la connexion a été fermée. Si le problème persiste, merci de notifier l'administrateur ou de regarder les journaux système.",
 
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
         "HELP_CLIPBOARD"           : "Texte copié/coupé dans Guacamole apparaîtra ici. Changer le texte ci dessous affectera le presse-papiers distant.",
         "HELP_INPUT_METHOD_NONE"   : "Aucune méthode de saisie utilisée. Clavier accepté depuis un clavier physique connecté.",
         "HELP_INPUT_METHOD_OSK"    : "Affiche et utilise la saisie du clavier virtuel intégré dans Guacamole. Le clavier virtuel permet d'utiliser des combinaisons de touches autrement impossibles (comme Ctrl-Alt-Supp).",
@@ -356,6 +358,7 @@
 
         "SECTION_HEADER_AUTHENTICATION"     : "Authentification",
         "SECTION_HEADER_BASIC_PARAMETERS"   : "Paramètres de base",
+        "SECTION_HEADER_CLIPBOARD"          : "Presse-papiers",
         "SECTION_HEADER_DEVICE_REDIRECTION" : "Redirection Périphérique",
         "SECTION_HEADER_DISPLAY"            : "Affichage",
         "SECTION_HEADER_NETWORK"            : "Réseau",
@@ -406,6 +409,7 @@
         "NAME" : "SSH",
 
         "SECTION_HEADER_AUTHENTICATION" : "Authentification",
+        "SECTION_HEADER_CLIPBOARD"      : "Presse-papiers",
         "SECTION_HEADER_DISPLAY"        : "Affichage",
         "SECTION_HEADER_NETWORK"        : "Réseau",
         "SECTION_HEADER_SESSION"        : "Session / Environnement",
@@ -450,6 +454,7 @@
         "NAME" : "Telnet",
 
         "SECTION_HEADER_AUTHENTICATION" : "Authentification",
+        "SECTION_HEADER_CLIPBOARD"      : "Presse-papiers",
         "SECTION_HEADER_DISPLAY"        : "Affichage",
         "SECTION_HEADER_NETWORK"        : "Réseau"
 
@@ -624,7 +629,7 @@
         
         "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
 
-        "HELP_SESSIONS" : "Toutes les connexions actives Guacamole sont listées ici. Si vous souhaitez en fermer une ou plusieurs, sélectionner les et cliquer sur \"Fermer Sessions\". La fermeture d'une session déconnectera immédiatement l'utilisateur.", 
+        "HELP_SESSIONS" : "Cette page sera remplie avec des connexions actuellement actives. Les connexions répertoriées et la possibilité de supprimer ces connexions dépendent de votre niveau d'accès. Si vous souhaitez en fermer une ou plusieurs, sélectionner les et cliquer sur \"Fermer Sessions\". La fermeture d'une session déconnectera immédiatement l'utilisateur.", 
         
         "INFO_NO_SESSIONS" : "Pas de session ouverte",
 
diff --git a/guacamole/src/main/webapp/translations/it.json b/guacamole/src/main/webapp/translations/it.json
index 5b3f641..296ab26 100644
--- a/guacamole/src/main/webapp/translations/it.json
+++ b/guacamole/src/main/webapp/translations/it.json
@@ -87,6 +87,8 @@
         "ERROR_UPLOAD_31D"     : "Ci sono troppi file in coda per il trasferimento. Attendi che siano completati i trasferimenti in atto e riprova.",
         "ERROR_UPLOAD_DEFAULT" : "Si è verificato un errore sul server e la connessione è stata chiusa. Riprova o contatta il tuo amministratore di sistema.",
 
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
         "HELP_CLIPBOARD"           : "Il testo copiato/tagliato appare qui. I cambiamenti effettuati al testo qui sotto saranno riportati negli appunti remoti.",
         "HELP_INPUT_METHOD_NONE"   : "Non c'è nessun metodo di immissione. L'input da tastiera è accettato da una tastiera fisica connessa.",
         "HELP_INPUT_METHOD_OSK"    : "Mostra e accetta input dalla tastiera su schermo. La tastiera su schermo ti permette di scrivere combinazioni di tasti altrimenti mipossibli (ad esempio Ctrl-Alt-Canc).",
@@ -332,6 +334,7 @@
 
         "SECTION_HEADER_AUTHENTICATION"     : "Authentication",
         "SECTION_HEADER_BASIC_PARAMETERS"   : "Basic Settings",
+        "SECTION_HEADER_CLIPBOARD"          : "Appunti",
         "SECTION_HEADER_DEVICE_REDIRECTION" : "Device Redirection",
         "SECTION_HEADER_DISPLAY"            : "Display",
         "SECTION_HEADER_NETWORK"            : "Network",
@@ -372,6 +375,7 @@
         "NAME" : "SSH",
 
         "SECTION_HEADER_AUTHENTICATION" : "Authentication",
+        "SECTION_HEADER_CLIPBOARD"      : "Appunti",
         "SECTION_HEADER_DISPLAY"        : "Display",
         "SECTION_HEADER_NETWORK"        : "Network",
         "SECTION_HEADER_SFTP"           : "SFTP"
@@ -407,6 +411,7 @@
         "NAME" : "Telnet",
 
         "SECTION_HEADER_AUTHENTICATION" : "Authentication",
+        "SECTION_HEADER_CLIPBOARD"      : "Appunti",
         "SECTION_HEADER_DISPLAY"        : "Display",
         "SECTION_HEADER_NETWORK"        : "Network"
 
@@ -566,7 +571,7 @@
         
         "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
 
-        "HELP_SESSIONS" : "All currently-active Guacamole sessions are listed here. If you wish to kill one or more sessions, check the box next to those sessions and click \"Kill Sessions\". Killing a session will immediately disconnect the user from the associated connection.",
+        "HELP_SESSIONS" : "Questa pagina verrà popolata con connessioni attualmente attive. Le connessioni elencate e la possibilità di uccidere tali connessioni dipende dal tuo livello di accesso. Se desideri uccidere una o più sessioni, seleziona la casella accanto a quelle sessioni e fai clic su \"Uccidi sessioni \". L'uccisione di una sessione interromperà immediatamente l'utente dalla connessione associata.",
         
         "INFO_NO_SESSIONS" : "Nessuna sessione attiva",
 
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"
+
+    }
+
+}
diff --git a/guacamole/src/main/webapp/translations/nl.json b/guacamole/src/main/webapp/translations/nl.json
index 69a789a..27e3eef 100644
--- a/guacamole/src/main/webapp/translations/nl.json
+++ b/guacamole/src/main/webapp/translations/nl.json
@@ -91,6 +91,8 @@
         "ERROR_UPLOAD_31D"     : "Er worden momenteel te veel bestanden overdragen. Gelieve te wachten tot de bestaande bestandsoverdracht is voltooid, en probeer het opnieuw.",
         "ERROR_UPLOAD_DEFAULT" : "Er is een interne fout opgetreden op de Guacamole server, en de verbinding is beëindigd. Als het probleem aanhoudt, neem dan contact op met uw systeembeheerder of kijk in uw systeem logs.",
 
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
         "HELP_CLIPBOARD"           : "Tekst gekopieerd / geknipt binnen Guacamole zal hier verschijnen. Wijzigingen in onderstaande tekst zal externe klembord beïnvloeden.",
         "HELP_INPUT_METHOD_NONE"   : "Geen invoer methode gebruiken. Toetsenbord invoer wordt geaccepteerd van een aangesloten, fysiek toetsenbord.",
         "HELP_INPUT_METHOD_OSK"    : "Weergave en accepteren van invoer via het ingebouwde Guacamole on-screen toetsenbord. Dit toetsenbord op het scherm maakt toetscombinaties mogelijk die anders onmogelijk zijn (zoals Ctrl-Alt-Del).",
@@ -365,6 +367,7 @@
 
         "SECTION_HEADER_AUTHENTICATION"     : "Authenticatie",
         "SECTION_HEADER_BASIC_PARAMETERS"   : "Basis Instellingen",
+        "SECTION_HEADER_CLIPBOARD"          : "Klembord",
         "SECTION_HEADER_DEVICE_REDIRECTION" : "Apparaat Verbindingen",
         "SECTION_HEADER_DISPLAY"            : "Scherm",
         "SECTION_HEADER_NETWORK"            : "Netwerk",
@@ -422,6 +425,7 @@
         "NAME" : "SSH",
 
         "SECTION_HEADER_AUTHENTICATION" : "Authenticatie",
+        "SECTION_HEADER_CLIPBOARD"      : "Klembord",
         "SECTION_HEADER_DISPLAY"        : "Scherm",
         "SECTION_HEADER_NETWORK"        : "Netwerk",
         "SECTION_HEADER_RECORDING"      : "Scherm Opname",
@@ -474,6 +478,7 @@
         "NAME" : "Telnet",
 
         "SECTION_HEADER_AUTHENTICATION" : "Authenticatie",
+        "SECTION_HEADER_CLIPBOARD"      : "Klembord",
         "SECTION_HEADER_DISPLAY"        : "Scherm",
         "SECTION_HEADER_RECORDING"      : "Scherm Opname",
         "SECTION_HEADER_TYPESCRIPT"     : "Typescript (Tekst Sessie Opname)",
@@ -654,7 +659,7 @@
 
         "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
 
-        "HELP_SESSIONS" : "Alle Guacamole sessies die op dit moment actief zijn worden hier getoond. Als u een of meerdere sessies wilt beeindigen, vink die sessie(s) dan aan en klik op \"Beeindig Sessies\". Door het verbreken van een sessie verliest de gebruiker ogenblikkelijk het contact met die sessie(s).",
+        "HELP_SESSIONS" : "Deze pagina wordt gevuld met momenteel actieve verbindingen. De vermelde verbindingen en de mogelijkheid om die verbindingen te doden, zijn afhankelijk van uw toegangsniveau. Als u een of meerdere sessies wilt beeindigen, vink die sessie(s) dan aan en klik op \"Beeindig Sessies\". Door het verbreken van een sessie verliest de gebruiker ogenblikkelijk het contact met die sessie(s).",
 
         "INFO_NO_SESSIONS" : "Geen actieve sessies",
 
diff --git a/guacamole/src/main/webapp/translations/no.json b/guacamole/src/main/webapp/translations/no.json
index 30ea871..79d34dd 100644
--- a/guacamole/src/main/webapp/translations/no.json
+++ b/guacamole/src/main/webapp/translations/no.json
@@ -93,6 +93,8 @@
         "ERROR_UPLOAD_31D"     : "For mange filer blir overført. Vent til aktive overføringer fullfører og prøv igjen.",
         "ERROR_UPLOAD_DEFAULT" : "En intern feil har oppstått i Guacamole og forbindelsen er terminert. Kontakt systemadministrator dersom problemet fortsetter eller sjekk systemloggene dine.",
 
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
         "HELP_CLIPBOARD"           : "Tekst som er kopiert eller klippet i Guacamole vises her. Endringer i teksten under vil påvirke den eksterne utklippstavlen.",
         "HELP_INPUT_METHOD_NONE"   : "Ingen innenhet er brukt. Tastetrykk fra et fysisk tilkoblet tastatur blir akseptert.",
         "HELP_INPUT_METHOD_OSK"    : "Vis og aksepter tastetrykk fra det innebygde skjermtastaturet i Guacamole. Skjermtastaturet tillater tasting av tastekombinasjoner som ellers kan være umulig (f.eks. Ctrl-Alt-Del).",
@@ -348,6 +350,7 @@
 
         "SECTION_HEADER_AUTHENTICATION"     : "Autentisering",
         "SECTION_HEADER_BASIC_PARAMETERS"   : "Grunnleggende Innstillinger",
+        "SECTION_HEADER_CLIPBOARD"          : "Utklippstavle",
         "SECTION_HEADER_DEVICE_REDIRECTION" : "Enhet omdirigering",
         "SECTION_HEADER_DISPLAY"            : "Skjerm",
         "SECTION_HEADER_NETWORK"            : "Nettverk",
@@ -404,6 +407,7 @@
         "NAME" : "SSH",
 
         "SECTION_HEADER_AUTHENTICATION" : "Autentisering",
+        "SECTION_HEADER_CLIPBOARD"      : "Utklippstavle",
         "SECTION_HEADER_DISPLAY"        : "Skjerm",
         "SECTION_HEADER_NETWORK"        : "Nettverk",
         "SECTION_HEADER_RECORDING"      : "Skjermopptak",
@@ -455,6 +459,7 @@
         "NAME" : "Telnet",
 
         "SECTION_HEADER_AUTHENTICATION" : "Autentisering",
+        "SECTION_HEADER_CLIPBOARD"      : "Utklippstavle",
         "SECTION_HEADER_DISPLAY"        : "Skjerm",
         "SECTION_HEADER_RECORDING"      : "Skjermopptak",
         "SECTION_HEADER_TYPESCRIPT"     : "Typescript (Opptak av tekstsesjon)",
@@ -635,7 +640,7 @@
         
         "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
 
-        "HELP_SESSIONS" : "Alle aktive Guacamolesesjoner er listet opp her. Dersom du ønsker å avbryte en eller flere sesjoner haker du av boksen ved siden av sesjonen og klikker \"Avbryt sesjoner\". Avbrytes en sesjon vil brukeren umiddelbart kobles av den aktuelle sesjonen.",
+        "HELP_SESSIONS" : "Denne siden vil bli fylt med nåværende aktive forbindelser. Tilkoblingene oppført og evnen til å drepe disse tilkoblingene er avhengig av tilgangsnivået ditt. Dersom du ønsker å avbryte en eller flere sesjoner haker du av boksen ved siden av sesjonen og klikker \"Avbryt sesjoner\". Avbrytes en sesjon vil brukeren umiddelbart kobles av den aktuelle sesjonen.",
         
         "INFO_NO_SESSIONS" : "Ingen aktive sesjoner",
 
diff --git a/guacamole/src/main/webapp/translations/ru.json b/guacamole/src/main/webapp/translations/ru.json
index 4f04525..9e53f66 100644
--- a/guacamole/src/main/webapp/translations/ru.json
+++ b/guacamole/src/main/webapp/translations/ru.json
@@ -88,6 +88,8 @@
         "ERROR_UPLOAD_31D"     : "Слишком много файлов передается в настоящий момент. Подождите завершения текущих передач и повторите попытку снова.",
         "ERROR_UPLOAD_DEFAULT" : "Соединение было прервано из-за внутренней ошибки сервера. Пожалуйста, попробуйте повторить попытку позднее или обратитесь к администратору.",
 
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
         "HELP_CLIPBOARD"           : "Текст, скопированный или вырезанный внутри сеанса, появится в этом поле. Изменение текста также отразиться на буфере обмена удаленного рабочего стола.",
         "HELP_INPUT_METHOD_NONE"   : "Не выбран метод ввода. Ввод разрешен для физической клавиатуры.",
         "HELP_INPUT_METHOD_OSK"    : "Отображать и принимать ввод со встроенной экранной клавиатуры. Экранная клавиатура позволяет вводить любые комбинации, недоступные в других режимах (например Alt-Ctrl-Del).",
@@ -327,9 +329,11 @@
         "FIELD_OPTION_SERVER_LAYOUT_FR_FR_AZERTY" : "French (Azerty)",
         "FIELD_OPTION_SERVER_LAYOUT_IT_IT_QWERTY" : "Italian (Qwerty)",
         "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Swedish (Qwerty)",
-	      "FIELD_OPTION_SERVER_LAYOUT_RU_RU_QWERTY" : "Russian (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_RU_RU_QWERTY" : "Russian (Qwerty)",
 
-        "NAME" : "RDP"
+        "NAME" : "RDP",
+
+        "SECTION_HEADER_CLIPBOARD" : "Буфер обмена"
 
     },
 
@@ -362,7 +366,9 @@
         "FIELD_OPTION_FONT_SIZE_96"    : "96",
         "FIELD_OPTION_FONT_SIZE_EMPTY" : "",
 
-        "NAME" : "SSH"
+        "NAME" : "SSH",
+
+        "SECTION_HEADER_CLIPBOARD" : "Буфер обмена"
 
     },
 
@@ -393,7 +399,9 @@
         "FIELD_OPTION_FONT_SIZE_96"    : "96",
         "FIELD_OPTION_FONT_SIZE_EMPTY" : "",
 
-        "NAME" : "Telnet"
+        "NAME" : "Telnet",
+
+        "SECTION_HEADER_CLIPBOARD" : "Буфер обмена"
 
     },
 
@@ -547,7 +555,7 @@
 
         "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE",
 
-        "HELP_SESSIONS" : "Все активные в настоящий момент сессии Guacamole представлены здесь. Если вы хотите завершить одну или несколько сессий, выберите нужные сессии и нажмите на \"Завершить сессии\". Принудительное завершение сессий приведет к немедленному отключению пользователей, которые ими пользуются.",
+        "HELP_SESSIONS" : "Эта страница будет заполнена активными в настоящее время соединениями. Перечисленные соединения и возможность убивать эти соединения зависят от вашего уровня доступа. Если вы хотите завершить одну или несколько сессий, выберите нужные сессии и нажмите на \"Завершить сессии\". Принудительное завершение сессий приведет к немедленному отключению пользователей, которые ими пользуются.",
 
         "INFO_NO_SESSIONS" : "Нет активных сессий",
 
diff --git a/guacamole/src/main/webapp/translations/zh.json b/guacamole/src/main/webapp/translations/zh.json
new file mode 100644
index 0000000..cf25245
--- /dev/null
+++ b/guacamole/src/main/webapp/translations/zh.json
@@ -0,0 +1,776 @@
+{
+    
+    "NAME" : "简体中文",
+    
+    "APP" : {
+
+        "ACTION_ACKNOWLEDGE"        : "确定",
+        "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_NAVIGATE_BACK"      : "返回",
+        "ACTION_NAVIGATE_HOME"      : "首页",
+        "ACTION_SAVE"               : "保存",
+        "ACTION_SEARCH"             : "搜索",
+        "ACTION_SHARE"              : "共享",
+        "ACTION_UPDATE_PASSWORD"    : "更新密码",
+        "ACTION_VIEW_HISTORY"       : "历史",
+
+        "DIALOG_HEADER_ERROR" : "出错",
+
+        "ERROR_PASSWORD_BLANK"    : "密码不能留空。",
+        "ERROR_PASSWORD_MISMATCH" : "输入的密码不吻合。",
+        
+        "FIELD_HEADER_PASSWORD"       : "密码:",
+        "FIELD_HEADER_PASSWORD_AGAIN" : "重输密码:",
+
+        "FIELD_PLACEHOLDER_FILTER" : "过滤",
+
+        "FORMAT_DATE_TIME_PRECISE" : "yyyy-MM-dd HH:mm:ss",
+
+        "INFO_ACTIVE_USER_COUNT" : "正在被{USERS}用户使用。",
+
+        "TEXT_ANONYMOUS_USER"   : "匿名",
+        "TEXT_HISTORY_DURATION" : "{VALUE} {UNIT, select, second{秒} minute{分} hour{小时} day{天} other{}}"
+
+    },
+
+    "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服务器拒绝了本连接。请先关闭至少一个连接再重试。",
+        "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服务器拒绝了本连接。请先关闭至少一个连接再重试。",
+        "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服务器出现了内部错误而被终止。如果问题持续,请通知您的系统管理员,或检查您的系统日志。",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "HELP_CLIPBOARD"           : "复制/剪切的文本将出现在这里。对下面文本内容所作的修改将会影响远程电脑上的剪贴板。",
+        "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"          : "正在共享当前连接,并可被使用以下链接的任何人使用:",
+
+        "INFO_CONNECTION_SHARED" : "此连接已被共享。",
+        "INFO_NO_FILE_TRANSFERS" : "无文件传输任务。",
+
+        "NAME_INPUT_METHOD_NONE"   : "无输入法",
+        "NAME_INPUT_METHOD_OSK"    : "屏幕键盘",
+        "NAME_INPUT_METHOD_TEXT"   : "文本输入",
+        "NAME_KEY_CTRL"            : "Ctrl",
+        "NAME_KEY_ALT"             : "Alt",
+        "NAME_KEY_ESC"             : "Esc",
+        "NAME_KEY_TAB"             : "Tab",
+        "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}秒后重连……",
+        "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" : "缺省(XML)"
+    },
+
+    "FORM" : {
+
+        "FIELD_PLACEHOLDER_DATE" : "YYYY-MM-DD",
+        "FIELD_PLACEHOLDER_TIME" : "HH:MM:SS",
+
+        "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_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",
+
+        "INFO_READ_ONLY" : "对不起,不能编辑此用户的账户。",
+
+        "SECTION_HEADER_CONNECTIONS" : "连接",
+        "SECTION_HEADER_EDIT_USER"   : "编辑用户",
+        "SECTION_HEADER_PERMISSIONS" : "使用权限",
+
+        "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_DOMAIN"          : "域:",
+        "FIELD_HEADER_DPI"             : "分辨率(DPI):",
+        "FIELD_HEADER_DRIVE_PATH"      : "虚拟盘路径:",
+        "FIELD_HEADER_ENABLE_AUDIO_INPUT"         : "启用音频输入(话筒):",
+        "FIELD_HEADER_ENABLE_DESKTOP_COMPOSITION" : "启用桌面合成效果(Aero):",
+        "FIELD_HEADER_ENABLE_DRIVE"               : "启用虚拟盘:",
+        "FIELD_HEADER_ENABLE_FONT_SMOOTHING"      : "启用字体平滑(ClearType):",
+        "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" : "负载平衡信息/cookie:",
+        "FIELD_HEADER_PASSWORD"        : "密码:",
+        "FIELD_HEADER_PORT"            : "端口:",
+        "FIELD_HEADER_PRECONNECTION_BLOB" : "预连接BLOB(VM标识):",
+        "FIELD_HEADER_PRECONNECTION_ID"   : "RDP源标识:",
+        "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_HOSTNAME"              : "主机名:",
+        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "SFTP keepalive时间间隔:",
+        "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"    : "低色(16位)",
+        "FIELD_OPTION_COLOR_DEPTH_24"    : "真彩(24位)",
+        "FIELD_OPTION_COLOR_DEPTH_32"    : "真彩(32位)",
+        "FIELD_OPTION_COLOR_DEPTH_8"     : "256色",
+        "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "",
+
+        "FIELD_OPTION_RESIZE_METHOD_DISPLAY_UPDATE" : "“显示更新”虚拟通道(RDP 8.1+)",
+        "FIELD_OPTION_RESIZE_METHOD_EMPTY"          : "",
+        "FIELD_OPTION_RESIZE_METHOD_RECONNECT"      : "重新连接",
+
+        "FIELD_OPTION_SECURITY_ANY"   : "任意",
+        "FIELD_OPTION_SECURITY_EMPTY" : "",
+        "FIELD_OPTION_SECURITY_NLA"   : "NLA(网络级别认证)",
+        "FIELD_OPTION_SECURITY_RDP"   : "RDP加密",
+        "FIELD_OPTION_SECURITY_TLS"   : "TLS加密",
+
+        "FIELD_OPTION_SERVER_LAYOUT_DE_DE_QWERTZ" : "German (Qwertz)",
+        "FIELD_OPTION_SERVER_LAYOUT_EMPTY"        : "",
+        "FIELD_OPTION_SERVER_LAYOUT_EN_GB_QWERTY" : "UK English (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_EN_US_QWERTY" : "US English (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_ES_ES_QWERTY" : "Spanish (Qwerty)",
+        "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_IT_IT_QWERTY" : "Italian (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_JA_JP_QWERTY" : "Japanese (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_PT_BR_QWERTY" : "Portuguese Brazilian (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_SV_SE_QWERTY" : "Swedish (Qwerty)",
+        "FIELD_OPTION_SERVER_LAYOUT_TR_TR_QWERTY" : "Turkish-Q (Qwerty)",
+
+        "NAME" : "RDP",
+
+        "SECTION_HEADER_AUTHENTICATION"     : "认证",
+        "SECTION_HEADER_BASIC_PARAMETERS"   : "基础设置",
+        "SECTION_HEADER_CLIPBOARD"          : "剪贴板",
+        "SECTION_HEADER_DEVICE_REDIRECTION" : "设备重定向",
+        "SECTION_HEADER_DISPLAY"            : "显示",
+        "SECTION_HEADER_GATEWAY"            : "远程桌面网关",
+        "SECTION_HEADER_LOAD_BALANCING"     : "负载平衡",
+        "SECTION_HEADER_NETWORK"            : "网络",
+        "SECTION_HEADER_PERFORMANCE"        : "性能",
+        "SECTION_HEADER_PRECONNECTION_PDU"  : "预连接PDU / Hyper-V",
+        "SECTION_HEADER_RECORDING"          : "屏幕录像",
+        "SECTION_HEADER_REMOTEAPP"          : "RemoteApp",
+        "SECTION_HEADER_SFTP"               : "SFTP"
+
+    },
+
+    "PROTOCOL_SSH" : {
+
+        "FIELD_HEADER_BACKSPACE"    : "退格键发送:",
+        "FIELD_HEADER_COLOR_SCHEME" : "配色方案:",
+        "FIELD_HEADER_COMMAND"      : "运行命令:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "自动建立录像目录:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "自动建立打字稿目录:",
+        "FIELD_HEADER_FONT_NAME"   : "字体名:",
+        "FIELD_HEADER_FONT_SIZE"   : "字体大小:",
+        "FIELD_HEADER_ENABLE_SFTP" : "启用SFTP:",
+        "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" : "服务器keepalive时间间隔:",
+        "FIELD_HEADER_SFTP_ROOT_DIRECTORY"   : "文件浏览器根目录:",
+        "FIELD_HEADER_TYPESCRIPT_NAME" : "打字稿名:",
+        "FIELD_HEADER_TYPESCRIPT_PATH" : "打字稿路径:",
+
+        "FIELD_OPTION_BACKSPACE_EMPTY" : "",
+        "FIELD_OPTION_BACKSPACE_8"     : "退格键(Ctrl-H)",
+        "FIELD_OPTION_BACKSPACE_127"   : "删除键(Ctrl-?)",
+
+        "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "白底黑字",
+        "FIELD_OPTION_COLOR_SCHEME_EMPTY"       : "",
+        "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK"  : "黑底灰字",
+        "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "黑底绿字",
+        "FIELD_OPTION_COLOR_SCHEME_WHITE_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" : "SSH",
+
+        "SECTION_HEADER_AUTHENTICATION" : "认证",
+        "SECTION_HEADER_BEHAVIOR"       : "终端行为",
+        "SECTION_HEADER_CLIPBOARD"      : "剪贴板",
+        "SECTION_HEADER_DISPLAY"        : "显示",
+        "SECTION_HEADER_NETWORK"        : "网络",
+        "SECTION_HEADER_RECORDING"      : "屏幕录像",
+        "SECTION_HEADER_SESSION"        : "会话 / 环境",
+        "SECTION_HEADER_TYPESCRIPT"     : "打字稿(文本会话录像)",
+        "SECTION_HEADER_SFTP"           : "SFTP"
+
+    },
+
+    "PROTOCOL_TELNET" : {
+
+        "FIELD_HEADER_BACKSPACE"      : "退格键发送:",
+        "FIELD_HEADER_COLOR_SCHEME"   : "配色方案:",
+        "FIELD_HEADER_CREATE_RECORDING_PATH" : "自动建立录像目录:",
+        "FIELD_HEADER_CREATE_TYPESCRIPT_PATH" : "自动建立打字稿目录:",
+        "FIELD_HEADER_FONT_NAME"      : "字体名:",
+        "FIELD_HEADER_FONT_SIZE"      : "字体大小:",
+        "FIELD_HEADER_HOSTNAME"       : "主机名:",
+        "FIELD_HEADER_USERNAME"       : "用户名:",
+        "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_TYPESCRIPT_NAME" : "打字稿名:",
+        "FIELD_HEADER_TYPESCRIPT_PATH" : "打字稿路径:",
+
+        "FIELD_OPTION_BACKSPACE_EMPTY" : "",
+        "FIELD_OPTION_BACKSPACE_8"     : "退格键(Ctrl-H)",
+        "FIELD_OPTION_BACKSPACE_127"   : "删除键(Ctrl-?)",
+
+        "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "白底黑字",
+        "FIELD_OPTION_COLOR_SCHEME_EMPTY"       : "",
+        "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK"  : "黑底灰字",
+        "FIELD_OPTION_COLOR_SCHEME_GREEN_BLACK" : "黑底绿字",
+        "FIELD_OPTION_COLOR_SCHEME_WHITE_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" : "Telnet",
+
+        "SECTION_HEADER_AUTHENTICATION" : "认证",
+        "SECTION_HEADER_BEHAVIOR"       : "终端行为",
+        "SECTION_HEADER_CLIPBOARD"      : "剪贴板",
+        "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_HOSTNAME"              : "主机名:",
+        "FIELD_HEADER_SFTP_SERVER_ALIVE_INTERVAL" : "SFTP keepalive时间间隔:",
+        "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"    : "低色(16位)",
+        "FIELD_OPTION_COLOR_DEPTH_24"    : "真彩(24位)",
+        "FIELD_OPTION_COLOR_DEPTH_32"    : "真彩(32位)",
+        "FIELD_OPTION_COLOR_DEPTH_EMPTY" : "",
+
+        "FIELD_OPTION_CURSOR_EMPTY"  : "",
+        "FIELD_OPTION_CURSOR_LOCAL"  : "本地",
+        "FIELD_OPTION_CURSOR_REMOTE" : "远程",
+
+        "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"          : "音频",
+        "SECTION_HEADER_AUTHENTICATION" : "认证",
+        "SECTION_HEADER_CLIPBOARD"      : "剪贴板",
+        "SECTION_HEADER_DISPLAY"        : "显示",
+        "SECTION_HEADER_NETWORK"        : "网络",
+        "SECTION_HEADER_RECORDING"      : "屏幕录像",
+        "SECTION_HEADER_REPEATER"       : "VNC中继",
+        "SECTION_HEADER_SFTP"           : "SFTP"
+
+    },
+
+    "SETTINGS" : {
+
+        "SECTION_HEADER_SETTINGS" : "设置"
+
+    },
+
+    "SETTINGS_CONNECTION_HISTORY" : {
+
+        "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD",
+        "ACTION_SEARCH"   : "@:APP.ACTION_SEARCH",
+
+        "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER",
+
+        "FILENAME_HISTORY_CSV" : "历史.csv",
+
+        "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输入时,有可能需要更改设置。本设置可在Guacamole菜单内被单个连接的设定覆盖。",
+        "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_LANGUAGE"             : "在下方列表中选择Guacamole界面所使用的语言。可选用的语言决定于系统安装了什么语言。",
+        "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_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" : "该页面将填充当前活动的连接。 列出的连接和终止连接的能力取决于您的访问级别。如需终止一个或多个会话,勾选目标会话并点击“终止会话”。终止会话会立即断开对应用户的连接。",
+        
+        "INFO_NO_SESSIONS" : "无活动会话",
+
+        "SECTION_HEADER_SESSIONS" : "活动会话",
+        
+        "TABLE_HEADER_SESSION_CONNECTION_NAME" : "连接名",
+        "TABLE_HEADER_SESSION_REMOTEHOST"      : "远程主机",
+        "TABLE_HEADER_SESSION_STARTDATE"       : "开始时间",
+        "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_NAVIGATE_HOME"      : "@:APP.ACTION_NAVIGATE_HOME",
+        "ACTION_VIEW_HISTORY"       : "@:APP.ACTION_VIEW_HISTORY"
+
+    }
+
+}
diff --git a/guacamole/util/check-translation.py b/guacamole/util/check-translation.py
new file mode 100755
index 0000000..25de49c
--- /dev/null
+++ b/guacamole/util/check-translation.py
@@ -0,0 +1,309 @@
+#!/usr/bin/python
+#
+# 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.
+#
+
+import argparse
+import json
+import os
+import re
+import sys
+
+parser = argparse.ArgumentParser(description='Compares two JSON translation '
+        'files, as used by the Apache Guacamole web application, listing '
+        'the strings which appear to be missing or incorrect.')
+
+parser.add_argument('--no-missing', dest='check_missing', action='store_false',
+        help='Disables checking for strings which are present in ORIGINAL but '
+        'are missing from TRANSLATED. Assuming ORIGINAL represents the set of '
+        'strings actually used by the web application, these strings are '
+        'those which are missing and need to be defined for the translation '
+        'to be complete. By default, the comparison will check for missing '
+        'translations.')
+
+parser.add_argument('--no-unused', dest='check_unused', action='store_false',
+        help='Disables checking for strings which are present in TRANSLATED '
+        'but not in ORIGINAL. Assuming ORIGINAL represents the set of strings '
+        'actually used by the web application, these strings are those which '
+        'are defined by the translation but unused. By default, the '
+        'comparison will check for unused translations.')
+
+parser.add_argument('--check-copied', action='store_true', help='Enables '
+        'checking for strings defined in TRANSLATED which are identical to '
+        'the corresponding strings in ORIGINAL. Such strings may have been '
+        'incorrectly copied verbatim from the original without being '
+        'translated at all. It is also possible that both languages simply '
+        'use the same text for that string, and the string is correct. As '
+        'this test can produce false positives, it is disabled by default.')
+
+parser.add_argument('ORIGINAL', nargs='?', help='The JSON file which should '
+        'be used as the basis for comparison. This should be JSON which can '
+        'be expected to contain every string used by the web application and '
+        'no others. Typically, this will be the primary, original language of '
+        'the web application. In the case of Apache Guacamole, this should be '
+        'English. If omitted, the file "en.json" within the same directory '
+        'as TRANSLATED will be used by default.')
+
+parser.add_argument('TRANSLATED', help='The JSON file which should be '
+        'compared against ORIGINAL. This should be the JSON which has been '
+        'translated from ORIGINAL, and thus should contain the same set of '
+        'strings if the translation is complete.')
+
+args = parser.parse_args()
+
+def flatten_strings(translation, prefix=u''):
+    """Reads all translation strings from the given JSON, taking into account
+    namespacing, flattening nested namespaces into a single set of key/value
+    pairs.
+
+    For example, the following call:
+
+        flatten_strings({
+            u'TOP' : {
+                u'LETTERS' : {
+                    u'A' : u'A',
+                    u'B' : u'B'
+                },
+                u'NUMBERS' : {
+                    u'ONE' : u'1',
+                    u'TWO' : u'2',
+                    u'THREE' : u'3'
+                }
+            }
+        })
+
+    would return:
+
+        {
+            u'TOP.LETTERS.A' : u'A',
+            u'TOP.LETTERS.B' : u'B',
+            u'TOP.NUMBERS.ONE' : u'1',
+            u'TOP.NUMBERS.TWO' : u'2',
+            u'TOP.NUMBERS.THREE' : u'3'
+        }
+
+    Parameters
+    ----------
+    translation : dict or unicode
+        The dict object to read translation strings from, where each key is a
+        translation key or namespace and each value is a translation string or
+        a dict containing the translations nested within that namespace.
+        this object is simply a Unicode string, it will be assumed to be the
+        value of a translation string, and the prefix provided will be assumed
+        to be the name.
+
+    prefix : unicode, optional
+        The namespace prefix to apply to all translation strings within the
+        given object, if any. This parameter is optional. If omitted, an empty
+        string will be used.
+
+    Returns
+    -------
+    dict
+        An dict whose properties are the names of all translation strings
+        contained within the given object.
+
+    """
+
+    strings = {}
+
+    # If the provided object is a string, the prefix is the string name
+    if isinstance(translation, unicode):
+        strings[prefix] = translation
+        return strings
+
+    # Otherwise, if the prefix is non-empty, append a period for children
+    if prefix:
+        prefix += u'.'
+
+    # For each property of the given object, read all string names
+    for key, child in translation.items():
+
+        # Add all string names within the child under its prefix
+        for flattened, value in flatten_strings(child, prefix + key).items():
+            strings[flattened] = value
+
+    return strings
+
+class Translation:
+    """A set of namespaced translation strings read from a JSON file, as
+    supported by angular-translate and used by Apache Guacamole.
+
+    Attributes
+    ----------
+    lang_key : unicode
+        The unique key identifying the JSON translation file and the language
+        within that file. This will simply be the filename without the ".json"
+        extension.
+    lang_name : unicode
+        The name of the language as defined within the JSON translation file by
+        the special "NAME" key. Not all translations will define a "NAME", as
+        some translations (those provided by Guacamole extensions) are used as
+        overlays for the base translation for that language defined at the web
+        application level. If no "NAME" key is present, `lang_name` will be
+        `None`.
+    strings : dict
+        The flattened set of translation key/value pairs. Each key will contain
+        all applicable namespaces, separated by periods, as produced by
+        `flatten_strings()`. There will be no nested keys.
+
+    """
+
+
+    def __init__(self, path):
+        """
+        Parses the details and contents of the JSON translation file at the
+        given path.
+
+        Parameters
+        ----------
+        path : str
+            The path to the JSON file containing the translation to be read.
+
+        """
+
+        json_data = open(path).read()
+        filename = os.path.basename(path)
+        
+        self.lang_key  = os.path.splitext(filename)[0]
+        self.strings   = flatten_strings(json.loads(json_data))
+        self.lang_name = self.strings.get(u'NAME', None)
+
+    def get_missing(self, expected):
+        """Returns a list of translation keys which are present in the given
+        translation but missing from this translation.
+
+        Parameters
+        ----------
+        expected : Translation
+            The translation to compare this translation against.
+
+        Returns
+        -------
+        list
+            A list of translation keys which are present in the given
+            translation but are NOT present in this translation.
+
+        """
+        return [ key for key in expected.strings if not key in self.strings ]
+
+    def get_identical(self, other):
+        """Returns a list of translation keys which map to the same exact value
+        in both this translation and the given translation.
+
+        Parameters
+        ----------
+        other : Translation
+            The translation to compare this translation against.
+
+        Returns
+        -------
+        list
+            A list of translation keys which map to the same exact value in
+            both translations.
+
+        """
+        return [ key for key, value in self.strings.items()
+                if key in other.strings and other.strings[key] == value ]
+
+#
+# Translation keys which are expected to always be inherited from the base
+# translation and thus should be missing from all translations
+#
+
+expected_missing = {
+    u'APP.NAME',
+    u'APP.VERSION'
+}
+
+#
+# Regular expression which matches strings that are expected to be copied
+# verbatim
+#
+
+expected_copied = re.compile('|'.join([
+    '^$', # Empty string
+    '^@:', # References to other strings
+    '^\\d+$', # Numbers
+    '^(VNC|RDP|SSH|SFTP|Telnet)$', # Protocol names
+    '^(Apache )?Guacamole$' # Guacamole itself
+]))
+
+#
+# Read provided input files
+#
+
+orig = Translation(args.ORIGINAL
+        or '{}/en.json'.format(os.path.dirname(args.TRANSLATED)))
+
+trans = Translation(args.TRANSLATED)
+
+print u'Original language: {} ({})'.format(orig.lang_key, orig.lang_name)
+print u'Translation language: {} ({})'.format(trans.lang_key, trans.lang_name)
+
+# Ignore keys that are expected to be missing
+orig.strings = { key:value for key, value in orig.strings.items()
+        if key not in expected_missing }
+
+#
+# Perform requested tests
+#
+
+missing = trans.get_missing(orig) if args.check_missing else []
+unused = orig.get_missing(trans) if args.check_unused else []
+copied = orig.get_identical(trans) if args.check_copied else []
+
+# Exclude keys which are expected to be copied
+copied = [ key for key in copied
+        if not expected_copied.match(orig.strings[key]) ]
+
+#
+# Group any errors encountered by type
+#
+
+if missing:
+    print('\nThe following strings are missing from the translation and '
+          'should be added:\n')
+    for name in sorted(missing):
+        print '    {}'.format(name)
+
+if unused:
+    print('\nThe following strings are either NOT defined for the original '
+          'language or are expected to be inherited from the original '
+          'language and should be removed:\n')
+    for name in sorted(unused):
+        print '    {}'.format(name)
+
+if copied:
+    print('\nThe following strings are identical to the original language '
+          'and MIGHT be untranslated:\n')
+    for name in sorted(copied):
+        print '    {}'.format(name)
+
+#
+# Count total number of errors and summarize result
+#
+
+errors = len(missing) + len(unused) + len(copied)
+
+if errors:
+    print '\n{} error(s) total.'.format(errors)
+    sys.exit(1)
+
+print '\nCheck completed successfully. No errors.'
+
diff --git a/pom.xml b/pom.xml
index 480eb23..83cfa8e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -26,7 +26,7 @@
     <groupId>org.apache.guacamole</groupId>
     <artifactId>guacamole-client</artifactId>
     <packaging>pom</packaging>
-    <version>1.0.0</version>
+    <version>1.1.0</version>
     <name>guacamole-client</name>
     <url>http://guacamole.apache.org/</url>