GUAC-919: Implement period cleanup of sessions. Simplify TokenSessionMap implementation.

This commit is contained in:
Michael Jumper
2014-11-03 00:59:31 -08:00
parent cc4b458fdd
commit 44d924f1f9
4 changed files with 124 additions and 74 deletions

View File

@@ -26,6 +26,7 @@ import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.glyptodon.guacamole.GuacamoleException;
@@ -68,6 +69,11 @@ public class GuacamoleSession {
*/
private final Map<String, GuacamoleTunnel> tunnels = new ConcurrentHashMap<String, GuacamoleTunnel>();
/**
* The last time this session was accessed.
*/
private long lastAccessedTime;
/**
* Creates a new Guacamole session associated with the given user context.
*
@@ -78,6 +84,7 @@ public class GuacamoleSession {
*/
public GuacamoleSession(Credentials credentials, UserContext userContext) throws GuacamoleException {
this.lastAccessedTime = System.currentTimeMillis();
this.credentials = credentials;
this.userContext = userContext;
@@ -210,4 +217,22 @@ public class GuacamoleSession {
return tunnels.remove(uuid) != null;
}
/**
* Updates this session, marking it as accessed.
*/
public void access() {
lastAccessedTime = System.currentTimeMillis();
}
/**
* Returns the time this session was last accessed, as the number of
* milliseconds since midnight January 1, 1970 GMT. Session access must
* be explicitly marked through calls to the access() function.
*
* @return The time this session was last accessed.
*/
public long getLastAccessedTime() {
return lastAccessedTime;
}
}

View File

@@ -160,8 +160,8 @@ public class TunnelRequestService {
throws GuacamoleException {
// Get auth token and session
String authToken = request.getParameter("authToken");
final GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
final String authToken = request.getParameter("authToken");
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
// Get ID of connection
String id = request.getParameter("id");
@@ -259,18 +259,19 @@ public class TunnelRequestService {
// Associate socket with tunnel
GuacamoleTunnel tunnel = new GuacamoleTunnel(socket) {
/**
* The current clipboard state.
*/
private final ClipboardState clipboard = session.getClipboardState();
@Override
public GuacamoleReader acquireReader() {
// Monitor instructions which pertain to server-side events, if necessary
try {
if (GuacamoleProperties.getProperty(ClipboardRESTService.INTEGRATION_ENABLED, false))
if (GuacamoleProperties.getProperty(ClipboardRESTService.INTEGRATION_ENABLED, false)) {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
ClipboardState clipboard = session.getClipboardState();
return new MonitoringGuacamoleReader(clipboard, super.acquireReader());
}
}
catch (GuacamoleException e) {
logger.warn("Clipboard integration failed to initialize: {}", e.getMessage());
@@ -285,6 +286,8 @@ public class TunnelRequestService {
@Override
public void close() throws GuacamoleException {
GuacamoleSession session = authenticationService.getGuacamoleSession(authToken);
// Only close if not canceled
if (!notifyClose(session, this))
throw new GuacamoleException("Tunnel close canceled by listener.");

View File

@@ -22,9 +22,13 @@
package org.glyptodon.guacamole.net.basic.rest.auth;
import java.util.Date;
import java.util.HashMap;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.glyptodon.guacamole.GuacamoleException;
import org.glyptodon.guacamole.net.basic.GuacamoleSession;
import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties;
@@ -44,108 +48,120 @@ public class BasicTokenSessionMap implements TokenSessionMap {
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(BasicTokenSessionMap.class);
/**
* The last time a user with a specific auth token accessed the API.
* Executor service which runs the period session eviction task.
*/
private final Map<String, Long> lastAccessTimeMap = new HashMap<String, Long>();
private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
/**
* Keeps track of the authToken to GuacamoleSession mapping.
*/
private final Map<String, GuacamoleSession> sessionMap = new HashMap<String, GuacamoleSession>();
/**
* The session timeout configuration for an API session, in milliseconds.
*/
private final long SESSION_TIMEOUT;
private final Map<String, GuacamoleSession> sessionMap =
Collections.synchronizedMap(new LinkedHashMap<String, GuacamoleSession>(16, 0.75f, true));
/**
* Create a new BasicTokenGuacamoleSessionMap and initialize the session timeout value.
*/
public BasicTokenSessionMap() {
// Set up the SESSION_TIMEOUT value, with a one hour default.
long sessionTimeoutValue;
// Read session timeout from guacamole.properties
try {
sessionTimeoutValue = GuacamoleProperties.getProperty(BasicGuacamoleProperties.API_SESSION_TIMEOUT, 3600000l);
}
catch (GuacamoleException e) {
logger.error("Unexpected GuacamoleException caught while reading API_SESSION_TIMEOUT property. Defaulting to 1 hour.", e);
logger.error("Unable to read guacamole.properties: {}", e.getMessage());
logger.debug("Error while reading session timeout value.", e);
sessionTimeoutValue = 3600000l;
}
SESSION_TIMEOUT = sessionTimeoutValue;
// Check for expired sessions every minute
executor.scheduleAtFixedRate(new SessionEvictionTask(sessionTimeoutValue), 1, 1, TimeUnit.MINUTES);
}
/**
* Evict an authentication token from the map of logged in users and last
* access times.
*
* @param authToken The authentication token to evict.
*/
private void evict(String authToken) {
sessionMap.remove(authToken);
lastAccessTimeMap.remove(authToken);
}
/**
* Log that the user represented by this auth token has just used the API.
*
* @param authToken The authentication token to record access time for.
*/
private void logAccessTime(String authToken) {
lastAccessTimeMap.put(authToken, new Date().getTime());
}
/**
* Check if a session has timed out.
* @param authToken The auth token for the session.
* @return True if the session has timed out, false otherwise.
*/
private boolean isSessionActive(String authToken) {
GuacamoleSession session = sessionMap.get(authToken);
if (session == null)
return false;
/**
* Task which iterates through all active sessions, evicting those sessions
* which are beyond the session timeout. This is a fairly easy thing to do,
* since the session storage structure guarantees that sessions are always
* in descending order of age.
*/
private class SessionEvictionTask implements Runnable {
// A session is active if it has any active tunnels
if (session.hasTunnels())
return true;
/**
* The maximum allowed age of any session, in milliseconds.
*/
private final long sessionTimeout;
/**
* Creates a new task which automatically evicts sessions which are
* older than the specified timeout.
*
* @param sessionTimeout The maximum age of any session, in
* milliseconds.
*/
public SessionEvictionTask(long sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
if (!lastAccessTimeMap.containsKey(authToken))
return true;
long lastAccessTime = lastAccessTimeMap.get(authToken);
long currentTime = new Date().getTime();
return currentTime - lastAccessTime > SESSION_TIMEOUT;
@Override
public void run() {
// Get current time
long now = System.currentTimeMillis();
logger.debug("Checking for expired sessions...");
// For each session, remove sesions which have expired
Iterator<Map.Entry<String, GuacamoleSession>> entries = sessionMap.entrySet().iterator();
while (entries.hasNext()) {
Map.Entry<String, GuacamoleSession> entry = entries.next();
GuacamoleSession session = entry.getValue();
// Get elapsed time since last access
long age = now - session.getLastAccessedTime();
// If session is too old, evict it and check the next one
if (age >= sessionTimeout) {
logger.debug("Session \"{}\" has timed out.", entry.getKey());
entries.remove();
}
// Otherwise, no other sessions can possibly be old enough
else
break;
}
logger.debug("Session check complete.");
}
}
@Override
public GuacamoleSession get(String authToken) {
// If the session has timed out, evict the token and force the user to log in again
if (isSessionActive(authToken)) {
evict(authToken);
return null;
}
// Update the last access time and return the GuacamoleSession
logAccessTime(authToken);
return sessionMap.get(authToken);
GuacamoleSession session = sessionMap.get(authToken);
if (session != null)
session.access();
return session;
}
@Override
public void put(String authToken, GuacamoleSession session) {
// Update the last access time, and create the token/GuacamoleSession mapping
logAccessTime(authToken);
sessionMap.put(authToken, session);
}
@Override
public void shutdown() {
executor.shutdownNow();
}
}

View File

@@ -51,4 +51,10 @@ public interface TokenSessionMap {
*/
public GuacamoleSession get(String authToken);
/**
* Shuts down this session map, disallowing future sessions and reclaiming
* any resources.
*/
public void shutdown();
}