diff --git a/extensions/guacamole-history-recording-storage/.gitignore b/extensions/guacamole-history-recording-storage/.gitignore
new file mode 100644
index 000000000..1de9633ae
--- /dev/null
+++ b/extensions/guacamole-history-recording-storage/.gitignore
@@ -0,0 +1,3 @@
+src/main/resources/generated/
+target/
+*~
diff --git a/extensions/guacamole-history-recording-storage/.ratignore b/extensions/guacamole-history-recording-storage/.ratignore
new file mode 100644
index 000000000..e69de29bb
diff --git a/extensions/guacamole-history-recording-storage/pom.xml b/extensions/guacamole-history-recording-storage/pom.xml
new file mode 100644
index 000000000..27cca873d
--- /dev/null
+++ b/extensions/guacamole-history-recording-storage/pom.xml
@@ -0,0 +1,52 @@
+
+
+
+
+ 4.0.0
+ org.apache.guacamole
+ guacamole-history-recording-storage
+ jar
+ 1.4.0
+ guacamole-history-recording-storage
+ http://guacamole.apache.org/
+
+
+ org.apache.guacamole
+ extensions
+ 1.4.0
+ ../
+
+
+
+
+
+
+ org.apache.guacamole
+ guacamole-ext
+ 1.4.0
+ provided
+
+
+
+
+
diff --git a/extensions/guacamole-history-recording-storage/src/main/assembly/dist.xml b/extensions/guacamole-history-recording-storage/src/main/assembly/dist.xml
new file mode 100644
index 000000000..6ee3cd8c8
--- /dev/null
+++ b/extensions/guacamole-history-recording-storage/src/main/assembly/dist.xml
@@ -0,0 +1,53 @@
+
+
+
+
+ dist
+ ${project.artifactId}-${project.version}
+
+
+
+ tar.gz
+
+
+
+
+
+
+
+
+ target/licenses
+
+
+
+
+ target
+
+
+ *.jar
+
+
+
+
+
+
diff --git a/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/HistoryAuthenticationProvider.java b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/HistoryAuthenticationProvider.java
new file mode 100644
index 000000000..61c551956
--- /dev/null
+++ b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/HistoryAuthenticationProvider.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.history;
+
+import org.apache.guacamole.history.user.HistoryUserContext;
+import org.apache.guacamole.GuacamoleException;
+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.UserContext;
+
+/**
+ * AuthenticationProvider implementation which automatically associates history
+ * entries with session recordings, typescripts, etc. History association is
+ * determined by matching the history entry UUID with the filenames of files
+ * located within a standardized/configurable directory.
+ */
+public class HistoryAuthenticationProvider extends AbstractAuthenticationProvider {
+
+ @Override
+ public String getIdentifier() {
+ return "recording-storage";
+ }
+
+ @Override
+ public UserContext decorate(UserContext context,
+ AuthenticatedUser authenticatedUser, Credentials credentials)
+ throws GuacamoleException {
+ return new HistoryUserContext(context.self(), context);
+ }
+
+}
diff --git a/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnection.java b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnection.java
new file mode 100644
index 000000000..4f1067413
--- /dev/null
+++ b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnection.java
@@ -0,0 +1,71 @@
+/*
+ * 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.history.connection;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.ActivityRecordSet;
+import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.ConnectionRecord;
+import org.apache.guacamole.net.auth.DelegatingConnection;
+import org.apache.guacamole.net.auth.User;
+
+/**
+ * Connection implementation that automatically defines ActivityLogs for
+ * files that relate to history entries associated with the wrapped connection.
+ */
+public class HistoryConnection extends DelegatingConnection {
+
+ /**
+ * The current Guacamole user.
+ */
+ private final User currentUser;
+
+ /**
+ * Creates a new HistoryConnection that wraps the given connection,
+ * automatically associating history entries with ActivityLogs based on
+ * related files (session recordings, typescripts, etc.).
+ *
+ * @param currentUser
+ * The current Guacamole user.
+ *
+ * @param connection
+ * The connection to wrap.
+ */
+ public HistoryConnection(User currentUser, Connection connection) {
+ super(connection);
+ this.currentUser = currentUser;
+ }
+
+ /**
+ * Returns the connection wrapped by this HistoryConnection.
+ *
+ * @return
+ * The connection wrapped by this HistoryConnection.
+ */
+ public Connection getWrappedConnection() {
+ return getDelegateConnection();
+ }
+
+ @Override
+ public ActivityRecordSet getConnectionHistory() throws GuacamoleException {
+ return new RecordedConnectionActivityRecordSet(currentUser, super.getConnectionHistory());
+ }
+
+}
diff --git a/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnectionRecord.java b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnectionRecord.java
new file mode 100644
index 000000000..13c76bd6a
--- /dev/null
+++ b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/HistoryConnectionRecord.java
@@ -0,0 +1,309 @@
+/*
+ * 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.history.connection;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.net.MalformedURLException;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.environment.Environment;
+import org.apache.guacamole.environment.LocalEnvironment;
+import org.apache.guacamole.io.GuacamoleReader;
+import org.apache.guacamole.io.ReaderGuacamoleReader;
+import org.apache.guacamole.language.TranslatableMessage;
+import org.apache.guacamole.net.auth.ActivityLog;
+import org.apache.guacamole.net.auth.ConnectionRecord;
+import org.apache.guacamole.net.auth.DelegatingConnectionRecord;
+import org.apache.guacamole.net.auth.FileActivityLog;
+import org.apache.guacamole.properties.FileGuacamoleProperty;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * ConnectionRecord implementation that automatically defines ActivityLogs for
+ * files that relate to the wrapped record.
+ */
+public class HistoryConnectionRecord extends DelegatingConnectionRecord {
+
+ /**
+ * Logger for this class.
+ */
+ private static final Logger logger = LoggerFactory.getLogger(HistoryConnectionRecord.class);
+
+ /**
+ * The namespace for URL UUIDs as defined by RFC 4122.
+ */
+ private static final UUID UUID_NAMESPACE_URL = UUID.fromString("6ba7b811-9dad-11d1-80b4-00c04fd430c8");
+
+ /**
+ * The filename suffix of typescript timing files.
+ */
+ private static final String TIMING_FILE_SUFFIX = ".timing";
+
+ /**
+ * The default directory to search for associated session recordings, if
+ * not overridden with the "recording-search-path" property.
+ */
+ private static final File DEFAULT_RECORDING_SEARCH_PATH = new File("/var/lib/guacamole/recordings");
+
+ /**
+ * The directory to search for associated session recordings. By default,
+ * "/var/lib/guacamole/recordings" will be used.
+ */
+ private static final FileGuacamoleProperty RECORDING_SEARCH_PATH = new FileGuacamoleProperty() {
+
+ @Override
+ public String getName() {
+ return "recording-search-path";
+ }
+
+ };
+
+ /**
+ * The recording file associated with the wrapped connection record. This
+ * may be a single file or a directory that may contain any number of
+ * relevant recordings.
+ */
+ private final File recording;
+
+ /**
+ * Creates a new HistoryConnectionRecord that wraps the given
+ * ConnectionRecord, automatically associating ActivityLogs based on
+ * related files (session recordings, typescripts, etc.).
+ *
+ * @param record
+ * The ConnectionRecord to wrap.
+ *
+ * @throws GuacamoleException
+ * If the configured path for stored recordings cannot be read.
+ */
+ public HistoryConnectionRecord(ConnectionRecord record) throws GuacamoleException {
+ super(record);
+
+ Environment environment = LocalEnvironment.getInstance();
+ File recordingPath = environment.getProperty(RECORDING_SEARCH_PATH,
+ DEFAULT_RECORDING_SEARCH_PATH);
+
+ String uuid = record.getUUID().toString();
+ File recordingFile = new File(recordingPath, uuid);
+ this.recording = recordingFile.canRead() ? recordingFile : null;
+
+ }
+
+ /**
+ * Returns whether the given file appears to be a Guacamole session
+ * recording. As there is no standard extension for session recordings,
+ * this is determined by attempting to read a single Guacamole instruction
+ * from the file.
+ *
+ * @param file
+ * The file to test.
+ *
+ * @return
+ * true if the file appears to be a Guacamole session recording, false
+ * otherwise.
+ */
+ private boolean isSessionRecording(File file) {
+
+ try (Reader reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8)) {
+
+ GuacamoleReader guacReader = new ReaderGuacamoleReader(reader);
+ if (guacReader.readInstruction() != null)
+ return true;
+
+ }
+ catch (GuacamoleException e) {
+ logger.debug("File \"{}\" does not appear to be a session "
+ + "recording, as it could not be parsed as Guacamole "
+ + "protocol data.", file, e);
+ }
+ catch (IOException e) {
+ logger.warn("Possible session recording \"{}\" could not be "
+ + "identified as it cannot be read: {}", file, e.getMessage());
+ logger.debug("Possible session recording \"{}\" could not be read.", file, e);
+ }
+
+ return false;
+
+ }
+
+ /**
+ * Returns whether the given file appears to be a typescript (text
+ * recording of a terminal session). As there is no standard extension for
+ * session recordings, this is determined by testing whether there is an
+ * associated timing file. Guacamole will always include a timing file for
+ * its typescripts.
+ *
+ * @param file
+ * The file to test.
+ *
+ * @return
+ * true if the file appears to be a typescript, false otherwise.
+ */
+ private boolean isTypescript(File file) {
+ return new File(file.getAbsolutePath() + TIMING_FILE_SUFFIX).exists();
+ }
+
+ /**
+ * Returns whether the given file appears to be a typescript timing file.
+ * Typescript timing files have the standard extension ".timing".
+ *
+ * @param file
+ * The file to test.
+ *
+ * @return
+ * true if the file appears to be a typescript timing file, false
+ * otherwise.
+ */
+ private boolean isTypescriptTiming(File file) {
+ return file.getName().endsWith(TIMING_FILE_SUFFIX);
+ }
+
+ /**
+ * Returns the type of session recording or log contained within the given
+ * file by inspecting its name and contents.
+ *
+ * @param file
+ * The file to test.
+ *
+ * @return
+ * The type of session recording or log contained within the given
+ * file, or null if this cannot be determined.
+ */
+ private ActivityLog.Type getType(File file) {
+
+ if (isSessionRecording(file))
+ return ActivityLog.Type.GUACAMOLE_SESSION_RECORDING;
+
+ if (isTypescript(file))
+ return ActivityLog.Type.TYPESCRIPT;
+
+ if (isTypescriptTiming(file))
+ return ActivityLog.Type.TYPESCRIPT_TIMING;
+
+ return ActivityLog.Type.SERVER_LOG;
+
+ }
+
+ /**
+ * Returns a new ActivityLog instance representing the session recording or
+ * log contained within the given file. If the type of recording/log cannot
+ * be determined, or if the file is unreadable, null is returned.
+ *
+ * @param file
+ * The file to produce an ActivityLog instance for.
+ *
+ * @return
+ * A new ActivityLog instance representing the recording/log contained
+ * within the given file, or null if the file is unreadable or cannot
+ * be identified.
+ */
+ private ActivityLog getActivityLog(File file) {
+
+ // Verify file can actually be read
+ if (!file.canRead()) {
+ logger.warn("Ignoring file \"{}\" relevant to connection history "
+ + "record as it cannot be read.", file);
+ return null;
+ }
+
+ // Determine type of recording/log by inspecting file
+ ActivityLog.Type logType = getType(file);
+ if (logType == null) {
+ logger.warn("Recording/log type of \"{}\" cannot be determined.", file);
+ return null;
+ }
+
+ return new FileActivityLog(
+ logType,
+ new TranslatableMessage("RECORDING_STORAGE.INFO_" + logType.name()),
+ file
+ );
+
+ }
+
+ /**
+ * Adds an ActivityLog instance representing the session recording or log
+ * contained within the given file to the given map of logs. If no
+ * ActivityLog can be produced for the given file (it is unreadable or
+ * cannot be identified), this function has no effect.
+ *
+ * @param logs
+ * The map of logs to add the ActivityLog to.
+ *
+ * @param file
+ * The file to produce an ActivityLog instance for.
+ */
+ private void addActivityLog(Map logs, File file) {
+
+ ActivityLog log = getActivityLog(file);
+ if (log == null)
+ return;
+
+ // Convert file into deterministic name UUID within URL namespace
+ UUID fileUUID;
+ try {
+ byte[] urlBytes = file.toURI().toURL().toString().getBytes(StandardCharsets.UTF_8);
+ fileUUID = UUID.nameUUIDFromBytes(ByteBuffer.allocate(16 + urlBytes.length)
+ .putLong(UUID_NAMESPACE_URL.getMostSignificantBits())
+ .putLong(UUID_NAMESPACE_URL.getLeastSignificantBits())
+ .put(urlBytes)
+ .array());
+ }
+ catch (MalformedURLException e) {
+ logger.warn("Ignoring file \"{}\" as a unique URL and UUID for that file could not be generated: {}", e.getMessage());
+ logger.debug("URL for file \"{}\" could not be determined.", file, e);
+ return;
+ }
+
+ logs.put(fileUUID.toString(), log);
+
+ }
+
+ @Override
+ public Map getLogs() {
+
+ // Do nothing if there are no associated logs
+ if (recording == null)
+ return super.getLogs();
+
+ // Add associated log (or logs, if this is a directory)
+ Map logs = new HashMap<>(super.getLogs());
+ if (recording.isDirectory()) {
+ Arrays.asList(recording.listFiles()).stream()
+ .forEach((file) -> addActivityLog(logs, file));
+ }
+ else
+ addActivityLog(logs, recording);
+
+ return logs;
+
+ }
+
+}
diff --git a/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/RecordedConnectionActivityRecordSet.java b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/RecordedConnectionActivityRecordSet.java
new file mode 100644
index 000000000..5caffdfa7
--- /dev/null
+++ b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/connection/RecordedConnectionActivityRecordSet.java
@@ -0,0 +1,125 @@
+/*
+ * 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.history.connection;
+
+import java.util.Collections;
+import java.util.Set;
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.net.auth.ActivityRecordSet;
+import org.apache.guacamole.net.auth.ConnectionRecord;
+import org.apache.guacamole.net.auth.DecoratingActivityRecordSet;
+import org.apache.guacamole.net.auth.Permissions;
+import org.apache.guacamole.net.auth.User;
+import org.apache.guacamole.net.auth.permission.ObjectPermission;
+import org.apache.guacamole.net.auth.permission.SystemPermission;
+
+/**
+ * ActivityRecordSet implementation that automatically defines ActivityLogs for
+ * files that relate to history entries within the wrapped set.
+ */
+public class RecordedConnectionActivityRecordSet extends DecoratingActivityRecordSet {
+
+ /**
+ * Whether the current user is an administrator.
+ */
+ private final boolean isAdmin;
+
+ /**
+ * The overall set of connection permissions defined for the current user.
+ */
+ private final Set connectionPermissions;
+
+ /**
+ * Creates a new RecordedConnectionActivityRecordSet that wraps the given
+ * ActivityRecordSet, automatically associating history entries with
+ * ActivityLogs based on related files (session recordings, typescripts,
+ * etc.).
+ *
+ * @param currentUser
+ * The current Guacamole user.
+ *
+ * @param activityRecordSet
+ * The ActivityRecordSet to wrap.
+ *
+ * @throws GuacamoleException
+ * If the permissions for the current user cannot be retrieved.
+ */
+ public RecordedConnectionActivityRecordSet(User currentUser,
+ ActivityRecordSet activityRecordSet)
+ throws GuacamoleException {
+ super(activityRecordSet);
+
+ // Determine whether current user is an administrator
+ Permissions perms = currentUser.getEffectivePermissions();
+ isAdmin = perms.getSystemPermissions().hasPermission(SystemPermission.Type.ADMINISTER);
+
+ // If not an admin, additionally pull specific connection permissions
+ if (isAdmin)
+ connectionPermissions = Collections.emptySet();
+ else
+ connectionPermissions = perms.getConnectionPermissions().getPermissions();
+
+ }
+
+ /**
+ * Returns whether the current user has permission to view the logs
+ * associated with the given history record. It is already given that the
+ * user has permission to view the history record itself. This extension
+ * considers a user to have permission to view history logs if they are
+ * an administrator or if they have permission to edit the associated
+ * connection.
+ *
+ * @param record
+ * The record to check.
+ *
+ * @return
+ * true if the current user has permission to view the logs associated
+ * with the given record, false otherwise.
+ */
+ private boolean canViewLogs(ConnectionRecord record) {
+
+ // Administrator can always view
+ if (isAdmin)
+ return true;
+
+ // Non-administrator CANNOT view if permissions cannot be verified
+ String identifier = record.getConnectionIdentifier();
+ if (identifier == null)
+ return false;
+
+ // Non-administer can only view if they implicitly have permission to
+ // configure recordings (they have permission to edit)
+ ObjectPermission canUpdate = new ObjectPermission(ObjectPermission.Type.UPDATE, identifier);
+ return connectionPermissions.contains(canUpdate);
+
+ }
+
+ @Override
+ protected ConnectionRecord decorate(ConnectionRecord record) throws GuacamoleException {
+
+ // Provide access to logs only if permission is granted
+ if (canViewLogs(record))
+ return new HistoryConnectionRecord(record);
+
+ return record;
+
+ }
+
+}
diff --git a/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/user/HistoryUserContext.java b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/user/HistoryUserContext.java
new file mode 100644
index 000000000..62d639698
--- /dev/null
+++ b/extensions/guacamole-history-recording-storage/src/main/java/org/apache/guacamole/history/user/HistoryUserContext.java
@@ -0,0 +1,84 @@
+/*
+ * 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.history.user;
+
+import org.apache.guacamole.GuacamoleException;
+import org.apache.guacamole.history.connection.HistoryConnection;
+import org.apache.guacamole.history.connection.RecordedConnectionActivityRecordSet;
+import org.apache.guacamole.net.auth.ActivityRecordSet;
+import org.apache.guacamole.net.auth.Connection;
+import org.apache.guacamole.net.auth.ConnectionRecord;
+import org.apache.guacamole.net.auth.DecoratingDirectory;
+import org.apache.guacamole.net.auth.DelegatingUserContext;
+import org.apache.guacamole.net.auth.Directory;
+import org.apache.guacamole.net.auth.User;
+import org.apache.guacamole.net.auth.UserContext;
+
+/**
+ * UserContext implementation that automatically defines ActivityLogs for
+ * files that relate to history entries.
+ */
+public class HistoryUserContext extends DelegatingUserContext {
+
+ /**
+ * The current Guacamole user.
+ */
+ private final User currentUser;
+
+ /**
+ * Creates a new HistoryUserContext that wraps the given UserContext,
+ * automatically associating history entries with ActivityLogs based on
+ * related files (session recordings, typescripts, etc.).
+ *
+ * @param currentUser
+ * The current Guacamole user.
+ *
+ * @param context
+ * The UserContext to wrap.
+ */
+ public HistoryUserContext(User currentUser, UserContext context) {
+ super(context);
+ this.currentUser = currentUser;
+ }
+
+ @Override
+ public Directory getConnectionDirectory() throws GuacamoleException {
+ return new DecoratingDirectory(super.getConnectionDirectory()) {
+
+ @Override
+ protected Connection decorate(Connection object) {
+ return new HistoryConnection(currentUser, object);
+ }
+
+ @Override
+ protected Connection undecorate(Connection object) throws GuacamoleException {
+ return ((HistoryConnection) object).getWrappedConnection();
+ }
+
+ };
+ }
+
+ @Override
+ public ActivityRecordSet getConnectionHistory()
+ throws GuacamoleException {
+ return new RecordedConnectionActivityRecordSet(currentUser, super.getConnectionHistory());
+ }
+
+}
diff --git a/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json b/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json
new file mode 100644
index 000000000..51422359d
--- /dev/null
+++ b/extensions/guacamole-history-recording-storage/src/main/resources/guac-manifest.json
@@ -0,0 +1,16 @@
+{
+
+ "guacamoleVersion" : "1.4.0",
+
+ "name" : "Session Recording Storage",
+ "namespace" : "recording-storage",
+
+ "authProviders" : [
+ "org.apache.guacamole.history.HistoryAuthenticationProvider"
+ ],
+
+ "translations" : [
+ "translations/en.json"
+ ]
+
+}
diff --git a/extensions/guacamole-history-recording-storage/src/main/resources/translations/en.json b/extensions/guacamole-history-recording-storage/src/main/resources/translations/en.json
new file mode 100644
index 000000000..13b7dba2e
--- /dev/null
+++ b/extensions/guacamole-history-recording-storage/src/main/resources/translations/en.json
@@ -0,0 +1,14 @@
+{
+
+ "DATA_SOURCE_RECORDING_STORAGE" : {
+ "NAME" : "Session Recording Storage"
+ },
+
+ "RECORDING_STORAGE" : {
+ "INFO_GUACAMOLE_SESSION_RECORDING" : "Graphical recording of remote desktop session",
+ "INFO_SERVER_LOG" : "Server/system log",
+ "INFO_TYPESCRIPT" : "Text recording of terminal session",
+ "INFO_TYPESCRIPT_TIMING" : "Timing information for text recording of terminal session"
+ }
+
+}
diff --git a/extensions/pom.xml b/extensions/pom.xml
index 966bf410f..de2b24556 100644
--- a/extensions/pom.xml
+++ b/extensions/pom.xml
@@ -50,6 +50,7 @@
guacamole-auth-totp
+ guacamole-history-recording-storage
guacamole-vault