diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java index 1b761d614..9c1f28bd4 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/GuacamoleSession.java @@ -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 tunnels = new ConcurrentHashMap(); + /** + * 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; + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java index 5c23b0455..dd657ffd1 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/TunnelRequestService.java @@ -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."); diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java index 331d9f3ff..e7c88ad55 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/BasicTokenSessionMap.java @@ -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 lastAccessTimeMap = new HashMap(); + private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); /** * Keeps track of the authToken to GuacamoleSession mapping. */ - private final Map sessionMap = new HashMap(); - - /** - * The session timeout configuration for an API session, in milliseconds. - */ - private final long SESSION_TIMEOUT; - + private final Map sessionMap = + Collections.synchronizedMap(new LinkedHashMap(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> entries = sessionMap.entrySet().iterator(); + while (entries.hasNext()) { + + Map.Entry 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(); } } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java index 9991aab52..cd4eb9b91 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/auth/TokenSessionMap.java @@ -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(); + }