Merge changes from 1.0.0 back to master.
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..1d3344d 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
@@ -111,13 +113,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 +126,8 @@
                 tunnel.close();
 
         }
+        else
+            throw new GuacamoleSecurityException("Permission denied.");
         
     }
 
@@ -162,4 +165,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/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/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/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..20ac299 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;
@@ -233,13 +232,6 @@
         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 +271,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 +439,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 +456,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);
@@ -504,6 +494,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 +648,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 +657,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 +670,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 +704,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 +754,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 +763,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/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..828b05e 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
@@ -93,7 +93,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 +107,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.
      */
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-ldap/pom.xml b/extensions/guacamole-auth-ldap/pom.xml
index 22d59bd..856f630 100644
--- a/extensions/guacamole-auth-ldap/pom.xml
+++ b/extensions/guacamole-auth-ldap/pom.xml
@@ -160,6 +160,14 @@
             <version>3.0</version>
         </dependency>
 
+        <!-- JUnit -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.12</version>
+            <scope>test</scope>
+        </dependency>
+
     </dependencies>
 
 </project>
diff --git a/extensions/guacamole-auth-ldap/src/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..d9d3ab1 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,14 +21,23 @@
 
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.novell.ldap.LDAPAttribute;
+import com.novell.ldap.LDAPAttributeSet;
 import com.novell.ldap.LDAPConnection;
+import com.novell.ldap.LDAPEntry;
+import com.novell.ldap.LDAPException;
+import java.util.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.guacamole.GuacamoleException;
+import org.apache.guacamole.GuacamoleServerException;
 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.net.auth.credentials.CredentialsInfo;
 import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
@@ -74,13 +83,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
@@ -197,7 +206,8 @@
 
     /**
      * 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,7 +220,7 @@
      *     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 {
 
         // Attempt bind
@@ -236,12 +246,11 @@
                             ldapConnection.getAuthenticationDN());
 
             // Return AuthenticatedUser if bind succeeds
-            AuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
-            authenticatedUser.init(credentials, effectiveGroups);
+            LDAPAuthenticatedUser authenticatedUser = authenticatedUserProvider.get();
+            authenticatedUser.init(credentials, getAttributeTokens(ldapConnection, credentials.getUsername()), effectiveGroups);
             return authenticatedUser;
 
         }
-
         // Always disconnect
         finally {
             ldapService.disconnect(ldapConnection);
@@ -250,6 +259,69 @@
     }
 
     /**
+     * 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(LDAPConnection ldapConnection,
+            String username) 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()]);
+        String userDN = getUserBindDN(username);
+
+        Map<String, String> tokens = new HashMap<String, String>();
+        try {
+
+            // Get LDAP attributes by querying LDAP
+            LDAPEntry userEntry = ldapConnection.read(userDN, attrArray);
+            if (userEntry == null)
+                return Collections.<String, String>emptyMap();
+
+            LDAPAttributeSet attrSet = userEntry.getAttributeSet();
+            if (attrSet == null)
+                return Collections.<String, String>emptyMap();
+
+            // Convert each retrieved attribute into a corresponding token
+            for (Object attrObj : attrSet) {
+                LDAPAttribute attr = (LDAPAttribute)attrObj;
+                tokens.put(TokenName.fromAttribute(attr.getName()), attr.getStringValue());
+            }
+
+        }
+        catch (LDAPException e) {
+            throw new GuacamoleServerException("Could not query LDAP user attributes.", e);
+        }
+
+        return tokens;
+
+    }
+
+    /**
      * Returns a UserContext object initialized with data accessible to the
      * given AuthenticatedUser.
      *
@@ -263,7 +335,7 @@
      * @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
@@ -275,7 +347,7 @@
         try {
 
             // Build user context by querying LDAP
-            UserContext userContext = userContextProvider.get();
+            LDAPUserContext userContext = userContextProvider.get();
             userContext.init(authenticatedUser, ldapConnection);
             return userContext;
 
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/ConfigurationService.java
index 6a4b8c0..e8ea0ac 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/ConfigurationService.java
@@ -245,7 +245,7 @@
     private int getMaxResults() throws GuacamoleException {
         return environment.getProperty(
             LDAPGuacamoleProperties.LDAP_MAX_SEARCH_RESULTS,
-            1000 
+            1000
         );
     }
 
@@ -362,4 +362,38 @@
         );
     }
 
+    /**
+     * Returns names for custom LDAP user attributes.
+     *
+     * @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/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/LDAPGuacamoleProperties.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/LDAPGuacamoleProperties.java
index 340cbf5..7529956 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/LDAPGuacamoleProperties.java
@@ -217,4 +217,24 @@
 
     };
 
+    /**
+     * 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/TokenName.java b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/TokenName.java
new file mode 100644
index 0000000..90de5bf
--- /dev/null
+++ b/extensions/guacamole-auth-ldap/src/main/java/org/apache/guacamole/auth/ldap/TokenName.java
@@ -0,0 +1,106 @@
+/*
+ * 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 java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Utility class for generating parameter token names.
+ */
+public class TokenName {
+
+    /**
+     * The prefix string to add to each parameter token generated from an LDAP
+     * attribute name.
+     */
+    private static final String LDAP_ATTRIBUTE_TOKEN_PREFIX = "LDAP_";
+
+    /**
+     * Pattern which matches logical groupings of words within an LDAP
+     * attribute name. This pattern is intended to match logical groupings
+     * regardless of the naming convention used: "CamelCase",
+     * "headlessCamelCase", "lowercase_with_underscores",
+     * "lowercase-with-dashes" or even "aVery-INCONSISTENTMix_ofAllStyles".
+     */
+    private static final Pattern LDAP_ATTRIBUTE_NAME_GROUPING = Pattern.compile(
+
+        // "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 value of the given LDAP attribute. The name of the LDAP attribute
+     * will automatically be transformed from "CamelCase", "headlessCamelCase",
+     * "lowercase_with_underscores", and "mixes_ofBoth_Styles" to consistent
+     * "UPPERCASE_WITH_UNDERSCORES". Each returned attribute will be prefixed
+     * with "LDAP_".
+     *
+     * @param name
+     *     The name of the LDAP attribute to use to generate the token name.
+     *
+     * @return
+     *     The name of the parameter token that should be populated with the
+     *     value of the LDAP attribute having the given name.
+     */
+    public static String fromAttribute(String name) {
+
+        // If even one logical word grouping cannot be found, default to
+        // simply converting the attribute to uppercase and adding the
+        // prefix
+        Matcher groupMatcher = LDAP_ATTRIBUTE_NAME_GROUPING.matcher(name);
+        if (!groupMatcher.find())
+            return LDAP_ATTRIBUTE_TOKEN_PREFIX + name.toUpperCase();
+
+        // Split the given name into logical word groups, separated by
+        // underscores and converted to uppercase
+        StringBuilder builder = new StringBuilder(LDAP_ATTRIBUTE_TOKEN_PREFIX);
+        builder.append(groupMatcher.group(0).toUpperCase());
+
+        while (groupMatcher.find()) {
+            builder.append("_");
+            builder.append(groupMatcher.group(0).toUpperCase());
+        }
+
+        return builder.toString();
+
+    }
+
+}
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..6a96d5b 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
@@ -35,12 +35,12 @@
 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;
 
@@ -127,10 +127,6 @@
             // 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);
-
             // Return a map of all readable connections
             return queryService.asMap(results, (entry) -> {
 
@@ -180,13 +176,17 @@
 
                 }
 
-                // 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.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;
 
             });
@@ -227,7 +227,11 @@
         StringBuilder connectionSearchFilter = new StringBuilder();
 
         // 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("(&(objectClass=guacConfigGroup)");
+        connectionSearchFilter.append("(|(");
+        connectionSearchFilter.append(escapingService.escapeLDAPSearchFilter(
+                confService.getMemberAttribute()));
+        connectionSearchFilter.append("=");
         connectionSearchFilter.append(escapingService.escapeLDAPSearchFilter(userDN));
         connectionSearchFilter.append(")");
 
@@ -246,4 +250,3 @@
     }
 
 }
-
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..3315beb 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
@@ -171,7 +171,7 @@
             ldapConnection,
             groupBaseDN,
             getGroupSearchFilter(),
-            Collections.singleton("member"),
+            Collections.singleton(confService.getMemberAttribute()),
             userDN
         );
 
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 68%
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..cafc461 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,6 +20,8 @@
 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.guacamole.net.auth.AbstractAuthenticatedUser;
 import org.apache.guacamole.net.auth.AuthenticationProvider;
@@ -29,7 +31,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,28 +46,53 @@
     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;
 
     /**
-     * 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.
      */
-    public void init(Credentials credentials, Set<String> effectiveGroups) {
+    public void init(Credentials credentials, Map<String, String> tokens, Set<String> effectiveGroups) {
         this.credentials = credentials;
+        this.tokens = Collections.unmodifiableMap(tokens);
         this.effectiveGroups = effectiveGroups;
         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;
+    }
+
     @Override
     public AuthenticationProvider getAuthenticationProvider() {
         return authProvider;
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 97%
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..5505f7e 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
@@ -46,12 +46,12 @@
  * An LDAP-specific implementation of UserContext which queries all Guacamole
  * connections and users from the LDAP directory.
  */
-public class UserContext extends AbstractUserContext {
+public class LDAPUserContext extends AbstractUserContext {
 
     /**
      * Logger for this class.
      */
-    private final Logger logger = LoggerFactory.getLogger(UserContext.class);
+    private final Logger logger = LoggerFactory.getLogger(LDAPUserContext.class);
 
     /**
      * Service for retrieving Guacamole connections from the LDAP server.
diff --git a/extensions/guacamole-auth-ldap/src/test/java/org/apache/guacamole/auth/ldap/TokenNameTest.java b/extensions/guacamole-auth-ldap/src/test/java/org/apache/guacamole/auth/ldap/TokenNameTest.java
new file mode 100644
index 0000000..2ba61dc
--- /dev/null
+++ b/extensions/guacamole-auth-ldap/src/test/java/org/apache/guacamole/auth/ldap/TokenNameTest.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;
+
+import static org.junit.Assert.assertEquals;
+import org.junit.Test;
+
+/**
+ * Test which verifies automatic generation of LDAP-specific connection
+ * parameter token names.
+ */
+public class TokenNameTest {
+
+    /**
+     * Verifies that TokenName.fromAttribute() generates token names as
+     * specified, regardless of the naming convention of the attribute.
+     */
+    @Test
+    public void testFromAttribute() {
+        assertEquals("LDAP_A", TokenName.fromAttribute("a"));
+        assertEquals("LDAP_B", TokenName.fromAttribute("b"));
+        assertEquals("LDAP_1", TokenName.fromAttribute("1"));
+        assertEquals("LDAP_SOME_URL", TokenName.fromAttribute("someURL"));
+        assertEquals("LDAP_LOWERCASE_WITH_DASHES", TokenName.fromAttribute("lowercase-with-dashes"));
+        assertEquals("LDAP_HEADLESS_CAMEL_CASE", TokenName.fromAttribute("headlessCamelCase"));
+        assertEquals("LDAP_CAMEL_CASE", TokenName.fromAttribute("CamelCase"));
+        assertEquals("LDAP_CAMEL_CASE", TokenName.fromAttribute("CamelCase"));
+        assertEquals("LDAP_LOWERCASE_WITH_UNDERSCORES", TokenName.fromAttribute("lowercase_with_underscores"));
+        assertEquals("LDAP_UPPERCASE_WITH_UNDERSCORES", TokenName.fromAttribute("UPPERCASE_WITH_UNDERSCORES"));
+        assertEquals("LDAP_A_VERY_INCONSISTENT_MIX_OF_ALL_STYLES", TokenName.fromAttribute("aVery-INCONSISTENTMix_ofAllStyles"));
+        assertEquals("LDAP_ABC_123_DEF_456", TokenName.fromAttribute("abc123def456"));
+        assertEquals("LDAP_ABC_123_DEF_456", TokenName.fromAttribute("ABC123DEF456"));
+        assertEquals("LDAP_WORD_A_WORD_AB_WORD_ABC_WORD", TokenName.fromAttribute("WordAWordABWordABCWord"));
+    }
+
+}
diff --git a/extensions/guacamole-auth-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/guacamole-common-js/src/main/webapp/modules/Client.js b/guacamole-common-js/src/main/webapp/modules/Client.js
index 85e9346..e947b70 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
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-docker/bin/start.sh b/guacamole-docker/bin/start.sh
index 6309265..9054a08 100755
--- a/guacamole-docker/bin/start.sh
+++ b/guacamole-docker/bin/start.sh
@@ -329,7 +329,7 @@
 start_guacamole() {
 
     # Install webapp
-    ln -sf /opt/guacamole/guacamole.war /usr/local/tomcat/webapps/
+    ln -sf /opt/guacamole/guacamole.war /usr/local/tomcat/webapps/${WEBAPP_CONTEXT:-guacamole}.war
 
     # Start tomcat
     cd /usr/local/tomcat
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..39b12f9 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,7 @@
 
 package org.apache.guacamole.net.auth;
 
+import java.util.Map;
 import org.apache.guacamole.GuacamoleException;
 import org.apache.guacamole.net.GuacamoleTunnel;
 import org.apache.guacamole.protocol.GuacamoleClientInformation;
@@ -31,11 +32,21 @@
     /**
      * Establishes a connection to guacd using the information associated with
      * this object. The connection will be provided the given client
-     * information.
+     * 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.
      *
@@ -43,8 +54,8 @@
      *     If an error occurs while connecting to guacd, or if permission to
      *     connect is denied.
      */
-    public GuacamoleTunnel connect(GuacamoleClientInformation info)
-            throws GuacamoleException;
+    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..b80e868 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
@@ -128,9 +128,9 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info)
-            throws GuacamoleException {
-        return connection.connect(info);
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+        return connection.connect(info, tokens);
     }
 
     @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..1d958bd 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
@@ -119,8 +119,9 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info) throws GuacamoleException {
-        return connectionGroup.connect(info);
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
+        return connectionGroup.connect(info, tokens);
     }
 
     @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..6d08d99 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,7 +160,7 @@
 
         // Get configurations
         Map<String, GuacamoleConfiguration> configs =
-                getFilteredAuthorizedConfigurations(authenticatedUser);
+                getAuthorizedConfigurations(authenticatedUser.getCredentials());
 
         // Return as unauthorized if not authorized to retrieve configs
         if (configs == null)
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..2dec26a 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,9 +38,14 @@
 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.
+ * An extremely basic Connection implementation. The underlying connection to
+ * guacd is established using the configuration information provided in
+ * guacamole.properties. Parameter tokens provided to connect() are
+ * automatically applied. Tracking of active connections and connection history
+ * is not provided.
  */
 public class SimpleConnection extends AbstractConnection {
 
@@ -95,8 +100,8 @@
     }
 
     @Override
-    public GuacamoleTunnel connect(GuacamoleClientInformation info)
-            throws GuacamoleException {
+    public GuacamoleTunnel connect(GuacamoleClientInformation info,
+            Map<String, String> tokens) throws GuacamoleException {
 
         // Retrieve proxy configuration from environment
         Environment environment = new LocalEnvironment();
@@ -106,6 +111,10 @@
         String hostname = proxyConfig.getHostname();
         int port = proxyConfig.getPort();
 
+        // Apply tokens to config parameters
+        GuacamoleConfiguration filteredConfig = new GuacamoleConfiguration(config);
+        new TokenFilter(tokens).filterValues(filteredConfig.getParameters());
+
         GuacamoleSocket socket;
 
         // Determine socket type based on required encryption method
@@ -115,7 +124,7 @@
             case SSL:
                 socket = new ConfiguredGuacamoleSocket(
                     new SSLGuacamoleSocket(hostname, port),
-                    config, info
+                    filteredConfig, info
                 );
                 break;
 
@@ -123,7 +132,7 @@
             case NONE:
                 socket = new ConfiguredGuacamoleSocket(
                     new InetGuacamoleSocket(hostname, port),
-                    config, info
+                    filteredConfig, info
                 );
                 break;
 
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..a077eb3 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
@@ -109,8 +109,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/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/resources/org/apache/guacamole/protocols/telnet.json b/guacamole-ext/src/main/resources/org/apache/guacamole/protocols/telnet.json
index 60064a7..42d5be0 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"
                 }
             ]
         },
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..ae8c463 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;
@@ -82,6 +84,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 +177,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 +207,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 +360,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 +402,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 +464,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 +519,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/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/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/TunnelRequestService.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java
index e023a70..1479d82 100644
--- a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java
+++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java
@@ -22,6 +22,7 @@
 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.GuacamoleSession;
@@ -187,6 +188,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.
      *
@@ -195,7 +200,7 @@
      */
     protected GuacamoleTunnel createConnectedTunnel(UserContext context,
             final TunnelRequest.Type type, String id,
-            GuacamoleClientInformation info)
+            GuacamoleClientInformation info, Map<String, String> tokens)
             throws GuacamoleException {
 
         // Create connected tunnel from identifier
@@ -216,7 +221,7 @@
                 }
 
                 // Connect tunnel
-                tunnel = connection.connect(info);
+                tunnel = connection.connect(info, tokens);
                 logger.info("User \"{}\" connected to connection \"{}\".", context.self().getIdentifier(), id);
                 break;
             }
@@ -235,7 +240,7 @@
                 }
 
                 // Connect tunnel
-                tunnel = group.connect(info);
+                tunnel = group.connect(info, tokens);
                 logger.info("User \"{}\" connected to group \"{}\".", context.self().getIdentifier(), id);
                 break;
             }
@@ -385,16 +390,17 @@
         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/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/form/directives/formField.js b/guacamole/src/main/webapp/app/form/directives/formField.js
index 9cf785c..ea0f35f 100644
--- a/guacamole/src/main/webapp/app/form/directives/formField.js
+++ b/guacamole/src/main/webapp/app/form/directives/formField.js
@@ -119,7 +119,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')
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/home/controllers/homeController.js b/guacamole/src/main/webapp/app/home/controllers/homeController.js
index f0e753d..6c2bd69 100644
--- a/guacamole/src/main/webapp/app/home/controllers/homeController.js
+++ b/guacamole/src/main/webapp/app/home/controllers/homeController.js
@@ -70,7 +70,7 @@
      */
     $scope.isLoaded = function isLoaded() {
 
-        return $scope.rootConnectionGroup !== null;
+        return $scope.rootConnectionGroups !== null;
 
     };
 
@@ -127,6 +127,6 @@
     )
     .then(function rootGroupsRetrieved(rootConnectionGroups) {
         $scope.rootConnectionGroups = rootConnectionGroups;
-    }, requestService.WARN);
+    }, requestService.DIE);
 
 }]);
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/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/navigation/directives/guacUserMenu.js b/guacamole/src/main/webapp/app/navigation/directives/guacUserMenu.js
index 492a867..5c6ad74 100644
--- a/guacamole/src/main/webapp/app/navigation/directives/guacUserMenu.js
+++ b/guacamole/src/main/webapp/app/navigation/directives/guacUserMenu.js
@@ -111,7 +111,7 @@
                 var email = user.attributes[User.Attributes.EMAIL_ADDRESS];
                 $scope.userURL = email ? 'mailto:' + email : null;
 
-            }, requestService.WARN);
+            }, requestService.DIE);
 
             /**
              * 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..2af426e 100644
--- a/guacamole/src/main/webapp/app/navigation/services/userPageService.js
+++ b/guacamole/src/main/webapp/app/navigation/services/userPageService.js
@@ -72,9 +72,11 @@
 
         // 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;
 
         // Determine whether a connection or balancing group should serve as
@@ -170,7 +172,7 @@
         })
         .then(function rootConnectionGroupsPermissionsRetrieved(data) {
             deferred.resolve(generateHomePage(data.rootGroups,data.permissionsSets));
-        }, requestService.WARN);
+        }, requestService.DIE);
 
         return deferred.promise;
 
@@ -195,7 +197,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 +277,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 +363,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 +444,7 @@
         .then(function rootConnectionGroupsRetrieved(retrievedRootGroups) {
             rootGroups = retrievedRootGroups;
             resolveMainPages();
-        }, requestService.WARN);
+        }, requestService.DIE);
 
         // Retrieve current permissions
         dataSourceService.apply(
@@ -459,7 +457,7 @@
         .then(function permissionsRetrieved(retrievedPermissions) {
             permissions = retrievedPermissions;
             resolveMainPages();
-        }, requestService.WARN);
+        }, requestService.DIE);
         
         return deferred.promise;
 
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/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..71e7af7 100644
--- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js
+++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsPreferences.js
@@ -178,7 +178,7 @@
                         value: languages[key]
                     };
                 });
-            }, requestService.WARN);
+            }, requestService.DIE);
 
             // Retrieve current permissions
             permissionService.getEffectivePermissions(dataSource, username)
diff --git a/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js b/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js
index 67776f0..5e1774d 100644
--- a/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js
+++ b/guacamole/src/main/webapp/app/settings/directives/guacSettingsSessions.js
@@ -189,12 +189,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 +222,7 @@
                 // Attempt to produce wrapped list of active connections
                 wrapAllActiveConnections();
 
-            }, requestService.WARN);
+            }, requestService.DIE);
             
             // Query active sessions
             dataSourceService.apply(
@@ -235,7 +237,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) {
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/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/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/index.html b/guacamole/src/main/webapp/index.html
index a4a93da..1d51606 100644
--- a/guacamole/src/main/webapp/index.html
+++ b/guacamole/src/main/webapp/index.html
@@ -32,26 +32,40 @@
     </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>
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/de.json b/guacamole/src/main/webapp/translations/de.json
index 90953e5..9e24ba7 100644
--- a/guacamole/src/main/webapp/translations/de.json
+++ b/guacamole/src/main/webapp/translations/de.json
@@ -621,7 +621,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..e80ac90 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.",
@@ -562,7 +563,10 @@
         "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:",
@@ -788,7 +792,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 +831,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..c165d5c 100644
--- a/guacamole/src/main/webapp/translations/es.json
+++ b/guacamole/src/main/webapp/translations/es.json
@@ -678,7 +678,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 +699,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..9407178 100644
--- a/guacamole/src/main/webapp/translations/fr.json
+++ b/guacamole/src/main/webapp/translations/fr.json
@@ -624,7 +624,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..442e709 100644
--- a/guacamole/src/main/webapp/translations/it.json
+++ b/guacamole/src/main/webapp/translations/it.json
@@ -566,7 +566,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/nl.json b/guacamole/src/main/webapp/translations/nl.json
index 69a789a..00ed1f6 100644
--- a/guacamole/src/main/webapp/translations/nl.json
+++ b/guacamole/src/main/webapp/translations/nl.json
@@ -654,7 +654,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..bdec130 100644
--- a/guacamole/src/main/webapp/translations/no.json
+++ b/guacamole/src/main/webapp/translations/no.json
@@ -635,7 +635,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..d6b2d47 100644
--- a/guacamole/src/main/webapp/translations/ru.json
+++ b/guacamole/src/main/webapp/translations/ru.json
@@ -547,7 +547,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..eca8656
--- /dev/null
+++ b/guacamole/src/main/webapp/translations/zh.json
@@ -0,0 +1,774 @@
+{
+    
+    "NAME" : "简体中文",
+    
+    "APP" : {
+
+        "NAME"    : "Apache Guacamole",
+        "VERSION" : "${project.version}",
+
+        "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服务器出现了内部错误而被终止。如果问题持续,请通知您的系统管理员,或检查您的系统日志。",
+
+        "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_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_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_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"
+
+    }
+
+}