GUACAMOLE-1152: Merge correct handling of client vs. server exceptions.

diff --git a/Dockerfile b/Dockerfile
index 0943dc3..2b70f67 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -25,7 +25,7 @@
 # such as `--build-arg TOMCAT_JRE=jre8-alpine`
 #
 ARG TOMCAT_VERSION=8.5
-ARG TOMCAT_JRE=jre8
+ARG TOMCAT_JRE=jdk8
 
 # Use official maven image for the build
 FROM maven:3-jdk-8 AS builder
diff --git a/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json b/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
index e34c3a6..dfee2f8 100644
--- a/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
+++ b/extensions/guacamole-auth-cas/src/main/resources/guac-manifest.json
@@ -14,18 +14,6 @@
         "translations/en.json",
         "translations/ja.json",
         "translations/ru.json"
-    ],
-
-    "js" : [
-        "cas.min.js"
-    ],
-
-    "css" : [
-        "cas.min.css"
-    ],
-
-    "resources" : {
-        "templates/casTicketField.html" : "text/html"
-    }
+    ]
 
 }
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java
index 4e4f35e..92bc541 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/MySQLAuthenticationProviderModule.java
@@ -95,7 +95,7 @@
         File trustStore = environment.getMySQLSSLTrustStore();
         if (trustStore != null)
             driverProperties.setProperty("trustCertificateKeyStoreUrl",
-                    trustStore.getAbsolutePath());
+                    trustStore.toURI().toString());
         
         String trustPassword = environment.getMySQLSSLTrustPassword();
         if (trustPassword != null)
@@ -105,7 +105,7 @@
         File clientStore = environment.getMySQLSSLClientStore();
         if (clientStore != null)
             driverProperties.setProperty("clientCertificateKeyStoreUrl",
-                    clientStore.getAbsolutePath());
+                    clientStore.toURI().toString());
         
         String clientPassword = environment.getMYSQLSSLClientPassword();
         if (clientPassword != null)
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java
index a27c2aa..f452319 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/org/apache/guacamole/auth/mysql/conf/MySQLEnvironment.java
@@ -369,7 +369,7 @@
      *     If guacamole.properties cannot be parsed.
      */
     public File getMySQLSSLClientStore() throws GuacamoleException {
-        return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_TRUST_STORE);
+        return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_CLIENT_STORE);
     }
     
     /**
@@ -384,7 +384,7 @@
      *     If guacamole.properties cannot be parsed.
      */
     public String getMYSQLSSLClientPassword() throws GuacamoleException {
-        return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_TRUST_PASSWORD);
+        return getProperty(MySQLGuacamoleProperties.MYSQL_SSL_CLIENT_PASSWORD);
     }
     
     @Override
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLAuthenticationProviderModule.java
index 331707c..280cead 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLAuthenticationProviderModule.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/PostgreSQLAuthenticationProviderModule.java
@@ -70,6 +70,15 @@
         myBatisProperties.setProperty("mybatis.pooled.pingEnabled", "true");
         myBatisProperties.setProperty("mybatis.pooled.pingQuery", "SELECT 1");
 
+        // Only set if > 0. Underlying backend does not take 0 as not-set.
+        int defaultStatementTimeout = environment.getPostgreSQLDefaultStatementTimeout();
+        if (defaultStatementTimeout > 0) {
+            myBatisProperties.setProperty(
+                "mybatis.configuration.defaultStatementTimeout",
+                String.valueOf(defaultStatementTimeout)
+            );
+        }
+
         // Use UTF-8 in database
         driverProperties.setProperty("characterEncoding", "UTF-8");
         
@@ -110,6 +119,12 @@
             
         }
 
+        // Handle case where TCP connection to database is silently dropped
+        driverProperties.setProperty(
+            "socketTimeout",
+            String.valueOf(environment.getPostgreSQLSocketTimeout())
+        );
+
     }
 
     @Override
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java
index e81e694..012877c 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLEnvironment.java
@@ -49,6 +49,19 @@
     private static final int DEFAULT_PORT = 5432;
 
     /**
+     * The default number of seconds the driver will wait for a response from
+     * the database, before aborting the query.
+     * A value of 0 (the default) means the timeout is disabled.
+     */
+    private static final int DEFAULT_STATEMENT_TIMEOUT = 0;
+
+    /**
+     * The default number of seconds to wait for socket read operations.
+     * A value of 0 (the default) means the timeout is disabled.
+     */
+    private static final int DEFAULT_SOCKET_TIMEOUT = 0;
+
+    /**
      * Whether a database user account is required by default for authentication
      * to succeed.
      */
@@ -249,6 +262,41 @@
     public String getPostgreSQLPassword() throws GuacamoleException {
         return getRequiredProperty(PostgreSQLGuacamoleProperties.POSTGRESQL_PASSWORD);
     }
+    
+    /**
+     * Returns the defaultStatementTimeout set for PostgreSQL connections.
+     * If unspecified, this will default to 0,
+     * and should not be passed through to the backend.
+     * 
+     * @return
+     *     The statement timeout (in seconds)
+     *
+     * @throws GuacamoleException 
+     *     If an error occurs while retrieving the property value.
+     */
+    public int getPostgreSQLDefaultStatementTimeout() throws GuacamoleException {
+        return getProperty(
+            PostgreSQLGuacamoleProperties.POSTGRESQL_DEFAULT_STATEMENT_TIMEOUT,
+            DEFAULT_STATEMENT_TIMEOUT
+        );
+    }
+    
+    /**
+     * Returns the socketTimeout property to set on PostgreSQL connections.
+     * If unspecified, this will default to 0 (no timeout)
+     * 
+     * @return
+     *     The socketTimeout to use when waiting on read operations (in seconds)
+     *
+     * @throws GuacamoleException 
+     *     If an error occurs while retrieving the property value.
+     */
+    public int getPostgreSQLSocketTimeout() throws GuacamoleException {
+        return getProperty(
+            PostgreSQLGuacamoleProperties.POSTGRESQL_SOCKET_TIMEOUT,
+            DEFAULT_SOCKET_TIMEOUT
+        );
+    }
 
     @Override
     public boolean isRecursiveQuerySupported(SqlSession session) {
diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java
index d2ae253..271d9c0 100644
--- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java
+++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-postgresql/src/main/java/org/apache/guacamole/auth/postgresql/conf/PostgreSQLGuacamoleProperties.java
@@ -95,6 +95,36 @@
     };
 
     /**
+     * The number of seconds the driver will wait for a response from
+     * the database, before aborting the query.
+     * A value of 0 (the default) means the timeout is disabled.
+     */
+    public static final IntegerGuacamoleProperty
+            POSTGRESQL_DEFAULT_STATEMENT_TIMEOUT = new IntegerGuacamoleProperty() {
+
+        @Override
+        public String getName() { return "postgresql-default-statement-timeout"; }
+
+    };
+
+    /**
+     * The number of seconds to wait for socket read operations.
+     * If reading from the server takes longer than this value, the
+     * connection will be closed. This can be used to handle network problems
+     * such as a dropped connection to the database. Similar to 
+     * postgresql-default-statement-timeout, it will have the effect of
+     * aborting queries that take too long.
+     * A value of 0 (the default) means the timeout is disabled.
+     */
+    public static final IntegerGuacamoleProperty
+            POSTGRESQL_SOCKET_TIMEOUT = new IntegerGuacamoleProperty() {
+
+        @Override
+        public String getName() { return "postgresql-socket-timeout"; }
+
+    };
+
+    /**
      * Whether a user account within the database is required for authentication
      * to succeed, even if the user has been authenticated via another
      * authentication provider.
diff --git a/guacamole-docker/bin/build-guacamole.sh b/guacamole-docker/bin/build-guacamole.sh
index 1b0b0ec..5ee3115 100755
--- a/guacamole-docker/bin/build-guacamole.sh
+++ b/guacamole-docker/bin/build-guacamole.sh
@@ -159,3 +159,28 @@
         --strip-components=1                               \
         "*.jar"
 fi
+
+#
+# Copy header auth extension if it was built
+#
+
+if [ -f extensions/guacamole-auth-header/target/guacamole-auth-header*.jar ]; then
+    mkdir -p "$DESTINATION/header"
+    cp extensions/guacamole-auth-header/target/guacamole-auth-header*.jar "$DESTINATION/header"
+fi
+
+#
+# Copy CAS auth extension if it was built
+#
+
+if [ -f extensions/guacamole-auth-cas/target/*.tar.gz ]; then
+    mkdir -p "$DESTINATION/cas"
+    tar -xzf extensions/guacamole-auth-cas/target/*.tar.gz  \
+    -C "$DESTINATION/cas/"                                  \
+    --wildcards                                             \
+    --no-anchored                                           \
+    --no-wildcards-match-slash                              \
+    --strip-components=1                                    \
+    "*.jar"
+fi
+
diff --git a/guacamole-docker/bin/start.sh b/guacamole-docker/bin/start.sh
index 3df1198..5123a8a 100755
--- a/guacamole-docker/bin/start.sh
+++ b/guacamole-docker/bin/start.sh
@@ -354,10 +354,18 @@
         "postgresql-default-max-group-connections-per-user" \
         "$POSTGRES_DEFAULT_MAX_GROUP_CONNECTIONS_PER_USER"
 
+    set_optional_property                      \
+        "postgresql-default-statement-timeout" \
+        "$POSTGRES_DEFAULT_STATEMENT_TIMEOUT"
+
     set_optional_property          \
         "postgresql-user-required" \
         "$POSTGRES_USER_REQUIRED"
 
+    set_optional_property           \
+        "postgresql-socket-timeout" \
+        "$POSTGRES_SOCKET_TIMEOUT"
+
     set_optional_property      \
         "postgresql-ssl-mode"  \
         "$POSTGRESQL_SSL_MODE"
@@ -413,35 +421,26 @@
     fi
 
     # Update config file
-    set_property          "ldap-hostname"           "$LDAP_HOSTNAME"
-    set_optional_property "ldap-port"               "$LDAP_PORT"
-    set_optional_property "ldap-encryption-method"  "$LDAP_ENCRYPTION_METHOD"
-    set_optional_property "ldap-max-search-results" "$LDAP_MAX_SEARCH_RESULTS"
-    set_optional_property "ldap-search-bind-dn"     "$LDAP_SEARCH_BIND_DN"
-    set_optional_property "ldap-user-attributes"    "$LDAP_USER_ATTRIBUTES"
+    set_property          "ldap-hostname"                   "$LDAP_HOSTNAME"
+    set_property          "ldap-user-base-dn"               "$LDAP_USER_BASE_DN"
 
-    set_optional_property           \
-        "ldap-search-bind-password" \
-        "$LDAP_SEARCH_BIND_PASSWORD"
-
-    set_property          "ldap-user-base-dn"       "$LDAP_USER_BASE_DN"
-    set_optional_property "ldap-username-attribute" "$LDAP_USERNAME_ATTRIBUTE"
-    set_optional_property "ldap-member-attribute"   "$LDAP_MEMBER_ATTRIBUTE"
-    set_optional_property "ldap-user-search-filter" "$LDAP_USER_SEARCH_FILTER"
-    set_optional_property "ldap-config-base-dn"     "$LDAP_CONFIG_BASE_DN"
-    set_optional_property "ldap-group-base-dn"      "$LDAP_GROUP_BASE_DN"
-
-    set_optional_property           \
-        "ldap-group-name-attribute" \
-        "$LDAP_GROUP_NAME_ATTRIBUTE"
-
-    set_optional_property           \
-        "ldap-dereference-aliases"  \
-        "$LDAP_DEREFERENCE_ALIASES"
-
-    set_optional_property "ldap-follow-referrals"   "$LDAP_FOLLOW_REFERRALS"
-    set_optional_property "ldap-max-referral-hops"  "$LDAP_MAX_REFERRAL_HOPS"
-    set_optional_property "ldap-operation-timeout"  "$LDAP_OPERATION_TIMEOUT"
+    set_optional_property "ldap-port"                       "$LDAP_PORT"
+    set_optional_property "ldap-encryption-method"          "$LDAP_ENCRYPTION_METHOD"
+    set_optional_property "ldap-max-search-results"         "$LDAP_MAX_SEARCH_RESULTS"
+    set_optional_property "ldap-search-bind-dn"             "$LDAP_SEARCH_BIND_DN"
+    set_optional_property "ldap-user-attributes"            "$LDAP_USER_ATTRIBUTES"
+    set_optional_property "ldap-search-bind-password"       "$LDAP_SEARCH_BIND_PASSWORD"
+    set_optional_property "ldap-username-attribute"         "$LDAP_USERNAME_ATTRIBUTE"
+    set_optional_property "ldap-member-attribute"           "$LDAP_MEMBER_ATTRIBUTE"
+    set_optional_property "ldap-user-search-filter"         "$LDAP_USER_SEARCH_FILTER"
+    set_optional_property "ldap-config-base-dn"             "$LDAP_CONFIG_BASE_DN"
+    set_optional_property "ldap-group-base-dn"              "$LDAP_GROUP_BASE_DN"
+    set_optional_property "ldap-member-attribute-type"      "$LDAP_MEMBER_ATTRIBUTE_TYPE"
+    set_optional_property "ldap-group-name-attribute"       "$LDAP_GROUP_NAME_ATTRIBUTE"
+    set_optional_property "ldap-dereference-aliases"        "$LDAP_DEREFERENCE_ALIASES"
+    set_optional_property "ldap-follow-referrals"           "$LDAP_FOLLOW_REFERRALS"
+    set_optional_property "ldap-max-referral-hops"          "$LDAP_MAX_REFERRAL_HOPS"
+    set_optional_property "ldap-operation-timeout"          "$LDAP_OPERATION_TIMEOUT"
 
     # Add required .jar files to GUACAMOLE_EXT
     ln -s /opt/guacamole/ldap/guacamole-auth-*.jar "$GUACAMOLE_EXT"
@@ -641,6 +640,50 @@
 }
 
 ##
+## Adds properties to guacamole.properties which configure the header
+## authentication provider.
+##
+associate_header() {
+    # Update config file
+    set_optional_property "http-auth-header"         "$HTTP_AUTH_HEADER"
+
+    # Add required .jar files to GUACAMOLE_EXT
+    ln -s /opt/guacamole/header/guacamole-auth-*.jar "$GUACAMOLE_EXT"
+}
+
+##
+## Adds properties to guacamole.properties witch configure the CAS
+## authentication service.
+##
+associate_cas() {
+    # Verify required parameters are present
+    if [ -z "$CAS_AUTHORIZATION_ENDPOINT" ] || \
+       [ -z "$CAS_REDIRECT_URI" ]
+    then
+        cat <<END
+FATAL: Missing required environment variables
+-----------------------------------------------------------------------------------
+If using the CAS authentication extension, you must provide each of the
+following environment variables:
+
+    CAS_AUTHORIZATION_ENDPOINT      The URL of the CAS authentication server.
+
+    CAS_REDIRECT_URI                The URI to redirect back to upon successful authentication.
+
+END
+        exit 1;
+    fi
+
+    # Update config file
+    set_property            "cas-authorization-endpoint"       "$CAS_AUTHORIZATION_ENDPOINT"
+    set_property            "cas-redirect-uri"                 "$CAS_REDIRECT_URI"
+    set_optional_property   "cas-clearpass-key"                "$CAS_CLEARPASS_KEY"
+
+    # Add required .jar files to GUACAMOLE_EXT
+    ln -s /opt/guacamole/cas/guacamole-auth-*.jar   "$GUACAMOLE_EXT"
+}
+
+##
 ## Starts Guacamole under Tomcat, replacing the current process with the
 ## Tomcat process. As the current process will be replaced, this MUST be the
 ## last function run within the script.
@@ -782,6 +825,16 @@
     associate_duo
 fi
 
+# Use header if specified.
+if [ "$HEADER_ENABLED" = "true" ]; then
+    associate_header
+fi
+
+# Use CAS if specified.
+if [ -n "$CAS_AUTHORIZATION_ENDPOINT" ]; then
+    associate_cas
+fi
+
 # Set logback level if specified
 if [ -n "$LOGBACK_LEVEL" ]; then
     unzip -o -j /opt/guacamole/guacamole.war WEB-INF/classes/logback.xml -d $GUACAMOLE_HOME
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityRecordSetResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityRecordSetResource.java
new file mode 100644
index 0000000..8599a95
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/history/ActivityRecordSetResource.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.rest.history;
+
+import java.util.ArrayList;
+import java.util.List;
+import javax.ws.rs.Consumes;
+import javax.ws.rs.GET;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.MediaType;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.ActivityRecord;
+import org.apache.guacamole.net.auth.ActivityRecordSet;
+
+/**
+ * A REST resource which abstracts the operations available on an
+ * ActivityRecordSet, such as the connection or user history available via the
+ * UserContext.
+ *
+ * @param <InternalRecordType>
+ *     The type of ActivityRecord that is contained
+ *     within the ActivityRecordSet represented by this resource. To avoid
+ *     coupling the REST API too tightly to the extension API, these objects
+ *     are not directly serialized or deserialized when handling REST requests.
+ *
+ * @param <ExternalRecordType>
+ *     The type of object used in interchange (ie: serialized/deserialized as
+ *     JSON) between REST clients and this resource to represent the
+ *     InternalRecordType.
+ */
+@Produces(MediaType.APPLICATION_JSON)
+@Consumes(MediaType.APPLICATION_JSON)
+public abstract class ActivityRecordSetResource<InternalRecordType extends ActivityRecord,
+        ExternalRecordType extends APIActivityRecord> {
+
+    /**
+     * The maximum number of history records to return in any one response.
+     */
+    private static final int MAXIMUM_HISTORY_SIZE = 1000;
+
+    /**
+     * The ActivityRecordSet whose records are being exposed.
+     */
+    private ActivityRecordSet<InternalRecordType> history;
+
+    /**
+     * Creates a new ActivityRecordSetResource which exposes the records within
+     * the given ActivityRecordSet.
+     *
+     * @param history
+     *     The ActivityRecordSet whose records should be exposed.
+     */
+    public ActivityRecordSetResource(ActivityRecordSet<InternalRecordType> history) {
+        this.history = history;
+    }
+
+    /**
+     * Converts the given internal record object to a record object which is
+     * decoupled from the extension API and is intended to be used in
+     * interchange via the REST API.
+     *
+     * @param record
+     *     The record to convert for the sake of interchange.
+     *
+     * @return
+     *     A new record object containing the same data as the given internal
+     *     record, but intended for use in interchange.
+     */
+    protected abstract ExternalRecordType toExternalRecord(InternalRecordType record);
+
+    /**
+     * Retrieves the list of activity records stored within the underlying
+     * ActivityRecordSet which match the given, arbitrary criteria. If
+     * specified, the returned records will also be sorted according to the
+     * given sort predicates.
+     *
+     * @param requiredContents
+     *     The set of strings that each must occur somewhere within the
+     *     returned records, whether within the associated username,
+     *     the name of some associated object (such as a connection), or any
+     *     associated date. If non-empty, any record not matching each of the
+     *     strings within the collection will be excluded from the results.
+     *
+     * @param sortPredicates
+     *     A list of predicates to apply while sorting the resulting records,
+     *     describing the properties involved and the sort order for those
+     *     properties.
+     *
+     * @return
+     *     The list of records which match the provided criteria, optionally
+     *     sorted as specified.
+     *
+     * @throws GuacamoleException
+     *     If an error occurs while applying the given filter criteria or
+     *     sort predicates.
+     */
+    @GET
+    public List<ExternalRecordType> getRecords(
+            @QueryParam("contains") List<String> requiredContents,
+            @QueryParam("order") List<APISortPredicate> sortPredicates)
+            throws GuacamoleException {
+
+        // Restrict to records which contain the specified strings
+        for (String required : requiredContents) {
+            if (!required.isEmpty())
+                history = history.contains(required);
+        }
+
+        // Sort according to specified ordering
+        for (APISortPredicate predicate : sortPredicates)
+            history = history.sort(predicate.getProperty(), predicate.isDescending());
+
+        // Limit to maximum result size
+        history = history.limit(MAXIMUM_HISTORY_SIZE);
+
+        // Convert record set to collection of API records
+        List<ExternalRecordType> apiRecords = new ArrayList<>();
+        for (InternalRecordType record : history.asCollection())
+            apiRecords.add(toExternalRecord(record));
+
+        // Return the converted history
+        return apiRecords;
+
+    }
+
+}
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/history/ConnectionHistoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/history/ConnectionHistoryResource.java
new file mode 100644
index 0000000..42e8a85
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/history/ConnectionHistoryResource.java
@@ -0,0 +1,49 @@
+/*
+ * 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.rest.history;
+
+import org.apache.guacamole.net.auth.ActivityRecordSet;
+import org.apache.guacamole.net.auth.ConnectionRecord;
+
+/**
+ * A REST resource for retrieving and managing the history records of Guacamole
+ * connections. Connection history records describe the start/end times of each
+ * usage of a connection (when a user connects and disconnects), as well as the
+ * specific user that connected/disconnected.
+ */
+public class ConnectionHistoryResource extends ActivityRecordSetResource<ConnectionRecord, APIConnectionRecord> {
+
+    /**
+     * Creates a new ConnectionHistoryResource which exposes the connection
+     * history records of the given ActivityRecordSet.
+     *
+     * @param history
+     *     The ActivityRecordSet whose records should be exposed.
+     */
+    public ConnectionHistoryResource(ActivityRecordSet<ConnectionRecord> history) {
+        super(history);
+    }
+
+    @Override
+    protected APIConnectionRecord toExternalRecord(ConnectionRecord record) {
+        return new APIConnectionRecord(record);
+    }
+
+}
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/history/HistoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/history/HistoryResource.java
index 559ebd5..8da8355 100644
--- a/guacamole/src/main/java/org/apache/guacamole/rest/history/HistoryResource.java
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/history/HistoryResource.java
@@ -19,18 +19,11 @@
 
 package org.apache.guacamole.rest.history;
 
-import java.util.ArrayList;
-import java.util.List;
 import javax.ws.rs.Consumes;
-import javax.ws.rs.GET;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
-import javax.ws.rs.QueryParam;
 import javax.ws.rs.core.MediaType;
 import org.apache.guacamole.GuacamoleException;
-import org.apache.guacamole.net.auth.ActivityRecord;
-import org.apache.guacamole.net.auth.ActivityRecordSet;
-import org.apache.guacamole.net.auth.ConnectionRecord;
 import org.apache.guacamole.net.auth.UserContext;
 
 /**
@@ -42,11 +35,6 @@
 public class HistoryResource {
 
     /**
-     * The maximum number of history records to return in any one response.
-     */
-    private static final int MAXIMUM_HISTORY_SIZE = 1000;
-
-    /**
      * The UserContext whose associated connection history is being exposed.
      */
     private final UserContext userContext;
@@ -63,114 +51,37 @@
     }
 
     /**
-     * Retrieves the usage history for all connections, restricted by optional
-     * filter parameters.
-     *
-     * @param requiredContents
-     *     The set of strings that each must occur somewhere within the
-     *     returned connection records, whether within the associated username,
-     *     the name of the associated connection, or any associated date. If
-     *     non-empty, any connection record not matching each of the strings
-     *     within the collection will be excluded from the results.
-     *
-     * @param sortPredicates
-     *     A list of predicates to apply while sorting the resulting connection
-     *     records, describing the properties involved and the sort order for
-     *     those properties.
+     * Retrieves the usage history for all connections. Filtering may be
+     * applied via the returned ConnectionHistoryResource.
      *
      * @return
-     *     A list of connection records, describing the start and end times of
-     *     various usages of this connection.
+     *     A resource which exposes connection records that may optionally be
+     *     filtered, each record describing the start and end times that a
+     *     particular connection was used.
      *
      * @throws GuacamoleException
      *     If an error occurs while retrieving the connection history.
      */
-    @GET
     @Path("connections")
-    public List<APIConnectionRecord> getConnectionHistory(
-            @QueryParam("contains") List<String> requiredContents,
-            @QueryParam("order") List<APISortPredicate> sortPredicates)
-            throws GuacamoleException {
-
-        // Retrieve overall connection history
-        ActivityRecordSet<ConnectionRecord> history = userContext.getConnectionHistory();
-
-        // Restrict to records which contain the specified strings
-        for (String required : requiredContents) {
-            if (!required.isEmpty())
-                history = history.contains(required);
-        }
-
-        // Sort according to specified ordering
-        for (APISortPredicate predicate : sortPredicates)
-            history = history.sort(predicate.getProperty(), predicate.isDescending());
-
-        // Limit to maximum result size
-        history = history.limit(MAXIMUM_HISTORY_SIZE);
-
-        // Convert record set to collection of API connection records
-        List<APIConnectionRecord> apiRecords = new ArrayList<APIConnectionRecord>();
-        for (ConnectionRecord record : history.asCollection())
-            apiRecords.add(new APIConnectionRecord(record));
-
-        // Return the converted history
-        return apiRecords;
-
+    public ConnectionHistoryResource getConnectionHistory() throws GuacamoleException {
+        return new ConnectionHistoryResource(userContext.getConnectionHistory());
     }
 
     /**
-     * Retrieves the login history for all users, restricted by optional filter
-     * parameters.
-     *
-     * @param requiredContents
-     *     The set of strings that each must occur somewhere within the
-     *     returned user records, whether within the associated username or any
-     *     associated date. If non-empty, any user record not matching each of
-     *     the strings within the collection will be excluded from the results.
-     *
-     * @param sortPredicates
-     *     A list of predicates to apply while sorting the resulting user
-     *     records, describing the properties involved and the sort order for
-     *     those properties.
+     * Retrieves the login history for all users. Filtering may be applied via
+     * the returned UserHistoryResource.
      *
      * @return
-     *     A list of user records, describing the start and end times of user
-     *     sessions.
+     *     A resource which exposes user records that may optionally be
+     *     filtered, each record describing the start and end times of a user
+     *     session.
      *
      * @throws GuacamoleException
      *     If an error occurs while retrieving the user history.
      */
-    @GET
     @Path("users")
-    public List<APIActivityRecord> getUserHistory(
-            @QueryParam("contains") List<String> requiredContents,
-            @QueryParam("order") List<APISortPredicate> sortPredicates)
-            throws GuacamoleException {
-
-        // Retrieve overall user history
-        ActivityRecordSet<ActivityRecord> history = userContext.getUserHistory();
-
-        // Restrict to records which contain the specified strings
-        for (String required : requiredContents) {
-            if (!required.isEmpty())
-                history = history.contains(required);
-        }
-
-        // Sort according to specified ordering
-        for (APISortPredicate predicate : sortPredicates)
-            history = history.sort(predicate.getProperty(), predicate.isDescending());
-
-        // Limit to maximum result size
-        history = history.limit(MAXIMUM_HISTORY_SIZE);
-
-        // Convert record set to collection of API user records
-        List<APIActivityRecord> apiRecords = new ArrayList<APIActivityRecord>();
-        for (ActivityRecord record : history.asCollection())
-            apiRecords.add(new APIActivityRecord(record));
-
-        // Return the converted history
-        return apiRecords;
-
+    public UserHistoryResource getUserHistory() throws GuacamoleException {
+        return new UserHistoryResource(userContext.getUserHistory());
     }
 
 }
diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/history/UserHistoryResource.java b/guacamole/src/main/java/org/apache/guacamole/rest/history/UserHistoryResource.java
new file mode 100644
index 0000000..6324873
--- /dev/null
+++ b/guacamole/src/main/java/org/apache/guacamole/rest/history/UserHistoryResource.java
@@ -0,0 +1,48 @@
+/*
+ * 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.rest.history;
+
+import org.apache.guacamole.net.auth.ActivityRecord;
+import org.apache.guacamole.net.auth.ActivityRecordSet;
+
+/**
+ * A REST resource for retrieving and managing the history records of Guacamole
+ * user sessions. User session history records describe the start/end times of
+ * individual user sessions (when specific users logged in and out).
+ */
+public class UserHistoryResource extends ActivityRecordSetResource<ActivityRecord, APIActivityRecord> {
+
+    /**
+     * Creates a new UserHistoryResource which exposes the user session history
+     * records of the given ActivityRecordSet.
+     *
+     * @param history
+     *     The ActivityRecordSet whose records should be exposed.
+     */
+    public UserHistoryResource(ActivityRecordSet<ActivityRecord> history) {
+        super(history);
+    }
+
+    @Override
+    protected APIActivityRecord toExternalRecord(ActivityRecord record) {
+        return new APIActivityRecord(record);
+    }
+
+}
diff --git a/guacamole/src/main/webapp/app/client/controllers/clientController.js b/guacamole/src/main/webapp/app/client/controllers/clientController.js
index c8941b3..326d868 100644
--- a/guacamole/src/main/webapp/app/client/controllers/clientController.js
+++ b/guacamole/src/main/webapp/app/client/controllers/clientController.js
@@ -678,8 +678,8 @@
         // Deal with substitute key presses
         if (substituteKeysPressed[keysym]) {
             event.preventDefault();
-            delete substituteKeysPressed[keysym];
             $scope.$broadcast('guacSyntheticKeyup', substituteKeysPressed[keysym]);
+            delete substituteKeysPressed[keysym];
         }
 
         // Mark key as released
diff --git a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js
index 1d81773..b058270 100644
--- a/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js
+++ b/guacamole/src/main/webapp/app/manage/controllers/manageConnectionGroupController.js
@@ -242,7 +242,7 @@
         $scope.managementPermissions = ManagementPermissions.fromPermissionSet(
                     values.permissions,
                     PermissionSet.SystemPermissionType.CREATE_CONNECTION,
-                    PermissionSet.hasConnectionPermission,
+                    PermissionSet.hasConnectionGroupPermission,
                     identifier);
 
     }, requestService.DIE);