From b3a49dae056bf261fe188358ddcade1b003f3f26 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 10 Dec 2015 16:13:12 -0800 Subject: [PATCH 1/3] GUAC-1427: Deprecate and remove use of guacamole-common's GuacamoleSession, storing tunnels directly within GuacamoleHTTPTunnelServlet instead. --- .../example/DummyGuacamoleTunnelServlet.java | 9 - .../doc/example/ExampleTunnelServlet.java | 12 +- .../servlet/GuacamoleHTTPTunnelServlet.java | 240 +++++++++++------- .../guacamole/servlet/GuacamoleSession.java | 88 +++---- 4 files changed, 196 insertions(+), 153 deletions(-) diff --git a/doc/guacamole-example/src/main/java/org/glyptodon/guacamole/net/example/DummyGuacamoleTunnelServlet.java b/doc/guacamole-example/src/main/java/org/glyptodon/guacamole/net/example/DummyGuacamoleTunnelServlet.java index a34d47633..58479ac7b 100644 --- a/doc/guacamole-example/src/main/java/org/glyptodon/guacamole/net/example/DummyGuacamoleTunnelServlet.java +++ b/doc/guacamole-example/src/main/java/org/glyptodon/guacamole/net/example/DummyGuacamoleTunnelServlet.java @@ -23,7 +23,6 @@ package org.glyptodon.guacamole.net.example; import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.net.GuacamoleSocket; import org.glyptodon.guacamole.net.GuacamoleTunnel; @@ -32,7 +31,6 @@ import org.glyptodon.guacamole.net.SimpleGuacamoleTunnel; import org.glyptodon.guacamole.protocol.ConfiguredGuacamoleSocket; import org.glyptodon.guacamole.protocol.GuacamoleConfiguration; import org.glyptodon.guacamole.servlet.GuacamoleHTTPTunnelServlet; -import org.glyptodon.guacamole.servlet.GuacamoleSession; /** * Simple tunnel example with hard-coded configuration parameters. @@ -44,8 +42,6 @@ public class DummyGuacamoleTunnelServlet extends GuacamoleHTTPTunnelServlet { @Override protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException { - HttpSession httpSession = request.getSession(true); - // guacd connection information String hostname = "localhost"; int port = 4822; @@ -65,11 +61,6 @@ public class DummyGuacamoleTunnelServlet extends GuacamoleHTTPTunnelServlet { // Create tunnel from now-configured socket GuacamoleTunnel tunnel = new SimpleGuacamoleTunnel(socket); - - // Attach tunnel - GuacamoleSession session = new GuacamoleSession(httpSession); - session.attachTunnel(tunnel); - return tunnel; } diff --git a/guacamole-common/doc/example/ExampleTunnelServlet.java b/guacamole-common/doc/example/ExampleTunnelServlet.java index 367ceda4c..88fd57b1c 100644 --- a/guacamole-common/doc/example/ExampleTunnelServlet.java +++ b/guacamole-common/doc/example/ExampleTunnelServlet.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -21,7 +21,6 @@ */ import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpSession; import net.sourceforge.guacamole.GuacamoleException; import net.sourceforge.guacamole.properties.GuacamoleProperties; import net.sourceforge.guacamole.net.GuacamoleSocket; @@ -29,7 +28,6 @@ import net.sourceforge.guacamole.net.GuacamoleTunnel; import net.sourceforge.guacamole.net.InetGuacamoleSocket; import net.sourceforge.guacamole.protocol.GuacamoleConfiguration; import net.sourceforge.guacamole.protocol.ConfiguredGuacamoleSocket; -import net.sourceforge.guacamole.servlet.GuacamoleSession; import net.sourceforge.guacamole.servlet.GuacamoleHTTPTunnelServlet; public class ExampleTunnelServlet extends GuacamoleHTTPTunnelServlet { @@ -38,8 +36,6 @@ public class ExampleTunnelServlet extends GuacamoleHTTPTunnelServlet { protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException { - HttpSession httpSession = request.getSession(true); - String hostname = GuacamoleProperties.getProperty( GuacamoleProperties.GUACD_HOSTNAME); @@ -57,12 +53,8 @@ public class ExampleTunnelServlet extends GuacamoleHTTPTunnelServlet { config ); + // Create and return tunnel GuacamoleTunnel tunnel = new GuacamoleTunnel(socket); - - // Attach tunnel - GuacamoleSession session = new GuacamoleSession(httpSession); - session.attachTunnel(tunnel); - return tunnel; } diff --git a/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelServlet.java b/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelServlet.java index 977438b48..3ff5119cb 100644 --- a/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelServlet.java +++ b/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelServlet.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,18 +22,18 @@ package org.glyptodon.guacamole.servlet; - import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpSession; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleConnectionClosedException; import org.glyptodon.guacamole.GuacamoleException; @@ -57,7 +57,13 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { /** * Logger for this class. */ - private Logger logger = LoggerFactory.getLogger(GuacamoleHTTPTunnelServlet.class); + private final Logger logger = LoggerFactory.getLogger(GuacamoleHTTPTunnelServlet.class); + + /** + * Map of absolutely all active tunnels using HTTP, indexed by tunnel UUID. + */ + private final ConcurrentMap tunnels = + new ConcurrentHashMap(); /** * The prefix of the query string which denotes a tunnel read operation. @@ -84,6 +90,56 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { */ private static final int UUID_LENGTH = 36; + /** + * Registers the given tunnel such that future read/write requests to that + * tunnel will be properly directed. + * + * @param tunnel + * The tunnel to register. + */ + protected void registerTunnel(GuacamoleTunnel tunnel) { + tunnels.put(tunnel.getUUID().toString(), tunnel); + logger.debug("Registered tunnel \"{}\".", tunnel.getUUID()); + } + + /** + * Deregisters the given tunnel such that future read/write requests to + * that tunnel will be rejected. + * + * @param tunnel + * The tunnel to deregister. + */ + protected void deregisterTunnel(GuacamoleTunnel tunnel) { + tunnels.remove(tunnel.getUUID().toString()); + logger.debug("Deregistered tunnel \"{}\".", tunnel.getUUID()); + } + + /** + * Returns the tunnel with the given UUID, if it has been registered with + * registerTunnel() and not yet deregistered with deregisterTunnel(). + * + * @param tunnelUUID + * The UUID of registered tunnel. + * + * @return + * The tunnel corresponding to the given UUID. + * + * @throws GuacamoleException + * If the requested tunnel does not exist because it has not yet been + * registered or it has been deregistered. + */ + protected GuacamoleTunnel getTunnel(String tunnelUUID) + throws GuacamoleException { + + // Pull tunnel from map + GuacamoleTunnel tunnel = tunnels.get(tunnelUUID); + if (tunnel == null) + throw new GuacamoleResourceNotFoundException("No such tunnel."); + + return tunnel; + + } + @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException { handleTunnelRequest(request, response); @@ -98,24 +154,29 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { * Sends an error on the given HTTP response using the information within * the given GuacamoleStatus. * - * @param response The HTTP response to use to send the error. - * @param guac_status The status to send - * @param message A human-readable message that can be presented to the - * user. - * @throws ServletException If an error prevents sending of the error - * code. + * @param response + * The HTTP response to use to send the error. + * + * @param guacStatus + * The status to send + * + * @param message + * A human-readable message that can be presented to the user. + * + * @throws ServletException + * If an error prevents sending of the error code. */ - public static void sendError(HttpServletResponse response, - GuacamoleStatus guac_status, String message) + protected void sendError(HttpServletResponse response, + GuacamoleStatus guacStatus, String message) throws ServletException { try { // If response not committed, send error code and message if (!response.isCommitted()) { - response.addHeader("Guacamole-Status-Code", Integer.toString(guac_status.getGuacamoleStatusCode())); + response.addHeader("Guacamole-Status-Code", Integer.toString(guacStatus.getGuacamoleStatusCode())); response.addHeader("Guacamole-Error-Message", message); - response.sendError(guac_status.getHttpStatusCode()); + response.sendError(guacStatus.getHttpStatusCode()); } } @@ -133,13 +194,19 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { * Dispatches every HTTP GET and POST request to the appropriate handler * function based on the query string. * - * @param request The HttpServletRequest associated with the GET or POST - * request received. - * @param response The HttpServletResponse associated with the GET or POST - * request received. - * @throws ServletException If an error occurs while servicing the request. + * @param request + * The HttpServletRequest associated with the GET or POST request + * received. + * + * @param response + * The HttpServletResponse associated with the GET or POST request + * received. + * + * @throws ServletException + * If an error occurs while servicing the request. */ - protected void handleTunnelRequest(HttpServletRequest request, HttpServletResponse response) throws ServletException { + protected void handleTunnelRequest(HttpServletRequest request, + HttpServletResponse response) throws ServletException { try { @@ -154,12 +221,8 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { GuacamoleTunnel tunnel = doConnect(request); if (tunnel != null) { - // Get session - HttpSession httpSession = request.getSession(true); - GuacamoleSession session = new GuacamoleSession(httpSession); - - // Attach tunnel to session - session.attachTunnel(tunnel); + // Register newly-created tunnel + registerTunnel(tunnel); try { // Ensure buggy browsers do not cache response @@ -215,48 +278,53 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { /** * Called whenever the JavaScript Guacamole client makes a connection - * request. It it up to the implementor of this function to define what - * conditions must be met for a tunnel to be configured and returned as a - * result of this connection request (whether some sort of credentials must - * be specified, for example). + * request via HTTP. It it up to the implementor of this function to define + * what conditions must be met for a tunnel to be configured and returned + * as a result of this connection request (whether some sort of credentials + * must be specified, for example). * - * @param request The HttpServletRequest associated with the connection - * request received. Any parameters specified along with - * the connection request can be read from this object. - * @return A newly constructed GuacamoleTunnel if successful, - * null otherwise. - * @throws GuacamoleException If an error occurs while constructing the - * GuacamoleTunnel, or if the conditions - * required for connection are not met. + * @param request + * The HttpServletRequest associated with the connection request + * received. Any parameters specified along with the connection request + * can be read from this object. + * + * @return + * A newly constructed GuacamoleTunnel if successful, null otherwise. + * + * @throws GuacamoleException + * If an error occurs while constructing the GuacamoleTunnel, or if the + * conditions required for connection are not met. */ - protected abstract GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException; + protected abstract GuacamoleTunnel doConnect(HttpServletRequest request) + throws GuacamoleException; /** * Called whenever the JavaScript Guacamole client makes a read request. * This function should in general not be overridden, as it already * contains a proper implementation of the read operation. * - * @param request The HttpServletRequest associated with the read request - * received. - * @param response The HttpServletResponse associated with the write request - * received. Any data to be sent to the client in response - * to the write request should be written to the response - * body of this HttpServletResponse. - * @param tunnelUUID The UUID of the tunnel to read from, as specified in - * the write request. This tunnel must be attached to - * the Guacamole session. - * @throws GuacamoleException If an error occurs while handling the read - * request. + * @param request + * The HttpServletRequest associated with the read request received. + * + * @param response + * The HttpServletResponse associated with the write request received. + * Any data to be sent to the client in response to the write request + * should be written to the response body of this HttpServletResponse. + * + * @param tunnelUUID + * The UUID of the tunnel to read from, as specified in the write + * request. This tunnel must have been created by a previous call to + * doConnect(). + * + * @throws GuacamoleException + * If an error occurs while handling the read request. */ - protected void doRead(HttpServletRequest request, HttpServletResponse response, String tunnelUUID) throws GuacamoleException { - - HttpSession httpSession = request.getSession(false); - GuacamoleSession session = new GuacamoleSession(httpSession); + protected void doRead(HttpServletRequest request, + HttpServletResponse response, String tunnelUUID) + throws GuacamoleException { // Get tunnel, ensure tunnel exists - GuacamoleTunnel tunnel = session.getTunnel(tunnelUUID); - if (tunnel == null) - throw new GuacamoleResourceNotFoundException("No such tunnel."); + GuacamoleTunnel tunnel = getTunnel(tunnelUUID); // Ensure tunnel is open if (!tunnel.isOpen()) @@ -280,8 +348,8 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { // Stream data to response, ensuring output stream is closed try { - // Detach tunnel and throw error if EOF (and we haven't sent any - // data yet. + // Deregister tunnel and throw error if we reach EOF without + // having ever sent any data char[] message = reader.read(); if (message == null) throw new GuacamoleConnectionClosedException("Tunnel reached end of stream."); @@ -306,7 +374,7 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { // Close tunnel immediately upon EOF if (message == null) { - session.detachTunnel(tunnel); + deregisterTunnel(tunnel); tunnel.close(); } @@ -319,8 +387,8 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { // Send end-of-stream marker and close tunnel if connection is closed catch (GuacamoleConnectionClosedException e) { - // Detach and close - session.detachTunnel(tunnel); + // Deregister and close + deregisterTunnel(tunnel); tunnel.close(); // End-of-instructions marker @@ -332,8 +400,8 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { catch (GuacamoleException e) { - // Detach and close - session.detachTunnel(tunnel); + // Deregister and close + deregisterTunnel(tunnel); tunnel.close(); throw e; @@ -350,8 +418,8 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { // Log typically frequent I/O error if desired logger.debug("Error writing to servlet output stream", e); - // Detach and close - session.detachTunnel(tunnel); + // Deregister and close + deregisterTunnel(tunnel); tunnel.close(); } @@ -366,25 +434,27 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { * This function should in general not be overridden, as it already * contains a proper implementation of the write operation. * - * @param request The HttpServletRequest associated with the write request - * received. Any data to be written will be specified within - * the body of this request. - * @param response The HttpServletResponse associated with the write request - * received. - * @param tunnelUUID The UUID of the tunnel to write to, as specified in - * the write request. This tunnel must be attached to - * the Guacamole session. - * @throws GuacamoleException If an error occurs while handling the write - * request. + * @param request + * The HttpServletRequest associated with the write request received. + * Any data to be written will be specified within the body of this + * request. + * + * @param response + * The HttpServletResponse associated with the write request received. + * + * @param tunnelUUID + * The UUID of the tunnel to write to, as specified in the write + * request. This tunnel must have been created by a previous call to + * doConnect(). + * + * @throws GuacamoleException + * If an error occurs while handling the write request. */ - protected void doWrite(HttpServletRequest request, HttpServletResponse response, String tunnelUUID) throws GuacamoleException { + protected void doWrite(HttpServletRequest request, + HttpServletResponse response, String tunnelUUID) + throws GuacamoleException { - HttpSession httpSession = request.getSession(false); - GuacamoleSession session = new GuacamoleSession(httpSession); - - GuacamoleTunnel tunnel = session.getTunnel(tunnelUUID); - if (tunnel == null) - throw new GuacamoleResourceNotFoundException("No such tunnel."); + GuacamoleTunnel tunnel = getTunnel(tunnelUUID); // We still need to set the content type to avoid the default of // text/html, as such a content type would cause some browsers to @@ -430,8 +500,8 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { } catch (IOException e) { - // Detach and close - session.detachTunnel(tunnel); + // Deregister and close + deregisterTunnel(tunnel); tunnel.close(); throw new GuacamoleServerException("I/O Error sending data to server: " + e.getMessage(), e); diff --git a/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleSession.java b/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleSession.java index 965112de9..7099cc855 100644 --- a/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleSession.java +++ b/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleSession.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013 Glyptodon LLC + * Copyright (C) 2015 Glyptodon LLC * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -22,12 +22,7 @@ package org.glyptodon.guacamole.servlet; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import javax.servlet.http.HttpSession; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,75 +32,70 @@ import org.slf4j.LoggerFactory; * * @author Michael Jumper */ +@Deprecated public class GuacamoleSession { /** * Logger for this class. */ - private Logger logger = LoggerFactory.getLogger(GuacamoleSession.class); + private final Logger logger = LoggerFactory.getLogger(GuacamoleSession.class); /** - * Map of all currently attached tunnels, indexed by tunnel UUID. - */ - private ConcurrentMap tunnels; - - /** - * Creates a new GuacamoleSession, storing and retrieving tunnels from the - * given HttpSession. Note that the true Guacamole session is tied to the - * HttpSession provided, thus creating a new GuacamoleSession does not - * create a new Guacamole session; it merely creates a new object for - * accessing the tunnels of an existing Guacamole session represented by - * the provided HttpSession. + * Creates a new GuacamoleSession. In prior versions of Guacamole, the + * GuacamoleSession object stored the tunnels associated with a particular + * user's use of the HTTP tunnel. The HTTP tunnel now stores all of these + * tunnels itself, and thus this class is no longer necessary. Its use will + * result in a warning being logged, and its functions will have no effect. * - * @param session The HttpSession to use as tunnel storage. - * @throws GuacamoleException If session is null. + * @param session + * The HttpSession that older versions of Guacamole would use as tunnel + * storage. This parameter is now ignored, and the GuacamoleSession + * class overall is deprecated. */ - @SuppressWarnings("unchecked") - public GuacamoleSession(HttpSession session) throws GuacamoleException { - - if (session == null) - throw new GuacamoleSecurityException("User has no session."); - - synchronized (session) { - - tunnels = (ConcurrentMap) session.getAttribute("GUAC_TUNNELS"); - if (tunnels == null) { - tunnels = new ConcurrentHashMap(); - session.setAttribute("GUAC_TUNNELS", tunnels); - } - - } - + public GuacamoleSession(HttpSession session) { + logger.warn("GuacamoleSession is deprecated. It is no longer " + + "necessary and its use will have no effect."); } /** - * Attaches the given tunnel to this GuacamoleSession. - * @param tunnel The tunnel to attach to this GucacamoleSession. + * Attaches the given tunnel to this GuacamoleSession. The GuacamoleSession + * class is now deprecated, and this function has no effect. + * + * @param tunnel + * The tunnel to attach to this GucacamoleSession. */ public void attachTunnel(GuacamoleTunnel tunnel) { - tunnels.put(tunnel.getUUID().toString(), tunnel); - logger.debug("Attached tunnel {}.", tunnel.getUUID()); + // Deprecated - no effect } /** - * Detaches the given tunnel to this GuacamoleSession. - * @param tunnel The tunnel to detach to this GucacamoleSession. + * Detaches the given tunnel to this GuacamoleSession. The GuacamoleSession + * class is now deprecated, and this function has no effect. + * + * @param tunnel + * The tunnel to detach to this GucacamoleSession. */ public void detachTunnel(GuacamoleTunnel tunnel) { - tunnels.remove(tunnel.getUUID().toString()); - logger.debug("Detached tunnel {}.", tunnel.getUUID()); + // Deprecated - no effect } /** * Returns the tunnel with the given UUID attached to this GuacamoleSession, - * if any. + * if any. The GuacamoleSession class is now deprecated, and this function + * has no effect. It will ALWAYS return null. * - * @param tunnelUUID The UUID of an attached tunnel. - * @return The tunnel corresponding to the given UUID, if attached, or null - * if no such tunnel is attached. + * @param tunnelUUID + * The UUID of an attached tunnel. + * + * @return + * The tunnel corresponding to the given UUID, if attached, or null if + * if no such tunnel is attached. */ public GuacamoleTunnel getTunnel(String tunnelUUID) { - return tunnels.get(tunnelUUID); + + // Deprecated - no effect + return null; + } } From 5b780fc8cfc6f8c0d534f0f0f6eed9f573b804be Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 10 Dec 2015 17:52:50 -0800 Subject: [PATCH 2/3] GUAC-1427: Implement automatic tracking and expiration of HTTP tunnels, independent of explicit read/write requests. --- .../servlet/GuacamoleHTTPTunnel.java | 78 +++++++ .../servlet/GuacamoleHTTPTunnelMap.java | 215 ++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnel.java create mode 100644 guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelMap.java diff --git a/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnel.java b/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnel.java new file mode 100644 index 000000000..40f47ea8b --- /dev/null +++ b/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnel.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.servlet; + +import org.glyptodon.guacamole.net.DelegatingGuacamoleTunnel; +import org.glyptodon.guacamole.net.GuacamoleTunnel; + +/** + * Tracks the last time a particular GuacamoleTunnel was accessed. This + * information is not necessary for tunnels associated with WebSocket + * connections, as each WebSocket connection has its own read thread which + * continuously checks the state of the tunnel and which will automatically + * timeout when the underlying socket times out, but the HTTP tunnel has no + * such thread. Because the HTTP tunnel requires the stream to be split across + * multiple requests, tracking of activity on the tunnel must be performed + * independently of the HTTP requests. + * + * @author Michael Jumper + */ +class GuacamoleHTTPTunnel extends DelegatingGuacamoleTunnel { + + /** + * The last time this tunnel was accessed. + */ + private long lastAccessedTime; + + /** + * Creates a new GuacamoleHTTPTunnel which wraps the given tunnel. + * Absolutely all function calls on this new GuacamoleHTTPTunnel will be + * delegated to the underlying GuacamoleTunnel. + * + * @param wrappedTunnel + * The GuacamoleTunnel to wrap within this GuacamoleHTTPTunnel. + */ + public GuacamoleHTTPTunnel(GuacamoleTunnel wrappedTunnel) { + super(wrappedTunnel); + } + + /** + * Updates this tunnel, marking it as recently accessed. + */ + public void access() { + lastAccessedTime = System.currentTimeMillis(); + } + + /** + * Returns the time this tunnel was last accessed, as the number of + * milliseconds since midnight January 1, 1970 GMT. Tunnel access must + * be explicitly marked through calls to the access() function. + * + * @return + * The time this tunnel was last accessed. + */ + public long getLastAccessedTime() { + return lastAccessedTime; + } + +} diff --git a/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelMap.java b/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelMap.java new file mode 100644 index 000000000..d7f53ce2b --- /dev/null +++ b/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelMap.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2015 Glyptodon LLC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package org.glyptodon.guacamole.servlet; + +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.GuacamoleTunnel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Map-style object which tracks in-use HTTP tunnels, automatically removing + * and closing tunnels which have not been used recently. This class is + * intended for use only within the GuacamoleHTTPTunnelServlet implementation, + * and has no real utility outside that implementation. + * + * @author Michael Jumper + */ +class GuacamoleHTTPTunnelMap { + + /** + * Logger for this class. + */ + private static final Logger logger = LoggerFactory.getLogger(GuacamoleHTTPTunnelMap.class); + + /** + * The number of seconds to wait between tunnel accesses before timing out + * Note that this will be enforced only within a factor of 2. If a tunnel + * is unused, it will take between TUNNEL_TIMEOUT and TUNNEL_TIMEOUT*2 + * seconds before that tunnel is closed and removed. + */ + private static final int TUNNEL_TIMEOUT = 15; + + /** + * Executor service which runs the periodic tunnel timeout task. + */ + private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(1); + + /** + * Map of all tunnels that are using HTTP, indexed by tunnel UUID. + */ + private final Map tunnelMap = + Collections.synchronizedMap(new LinkedHashMap(16, 0.75f, true)); + + /** + * Creates a new GuacamoleHTTPTunnelMap which automatically closes and + * removes HTTP tunnels which are no longer in use. + */ + public GuacamoleHTTPTunnelMap() { + + // Check for unused tunnels every few seconds + executor.scheduleAtFixedRate( + new TunnelTimeoutTask(TUNNEL_TIMEOUT * 1000l), + TUNNEL_TIMEOUT, TUNNEL_TIMEOUT, TimeUnit.SECONDS); + + } + + /** + * Task which iterates through all registered tunnels, removing and those + * tunnels which have not been accessed for a given number of milliseconds. + */ + private class TunnelTimeoutTask implements Runnable { + + /** + * The maximum amount of time to allow between accesses to any one + * HTTP tunnel, in milliseconds. + */ + private final long tunnelTimeout; + + /** + * Creates a new task which automatically closes and removes tunnels + * which have not been accessed for at least the given number of + * milliseconds. + * + * @param tunnelTimeout + * The maximum amount of time to allow between separate tunnel + * read/write requests, in milliseconds. + */ + public TunnelTimeoutTask(long tunnelTimeout) { + this.tunnelTimeout = tunnelTimeout; + } + + @Override + public void run() { + + // Get current time + long now = System.currentTimeMillis(); + + // For each tunnel, close and remove any tunnels which have expired + Iterator> entries = tunnelMap.entrySet().iterator(); + while (entries.hasNext()) { + + Map.Entry entry = entries.next(); + GuacamoleHTTPTunnel tunnel = entry.getValue(); + + // Get elapsed time since last access + long age = now - tunnel.getLastAccessedTime(); + + // If tunnel is too old, close and remove it + if (age >= tunnelTimeout) { + logger.debug("HTTP tunnel \"{}\" has timed out.", entry.getKey()); + entries.remove(); + + try { + tunnel.close(); + } + catch (GuacamoleException e) { + logger.debug("Unable to close expired HTTP tunnel.", e); + } + + } + + // Otherwise, this tunnel has been recently used, as have all + // other tunnels following this one within tunnelMap + else + break; + + } + + } + + } + + /** + * Returns the GuacamoleTunnel having the given UUID, wrapped within a + * GuacamoleHTTPTunnel. If the no tunnel having the given UUID is + * available, null is returned. + * + * @param uuid + * The UUID of the tunnel to retrieve. + * + * @return + * The GuacamoleTunnel having the given UUID, wrapped within a + * GuacamoleHTTPTunnel, if such a tunnel exists, or null if there is no + * such tunnel. + */ + public GuacamoleHTTPTunnel get(String uuid) { + + // Update the last access time + GuacamoleHTTPTunnel tunnel = tunnelMap.get(uuid); + if (tunnel != null) + tunnel.access(); + + // Return tunnel, if any + return tunnel; + + } + + /** + * Registers that a new connection has been established using HTTP via the + * given GuacamoleTunnel. + * + * @param uuid + * The UUID of the tunnel being added (registered). + * + * @param tunnel + * The GuacamoleTunnel being registered, its associated connection + * having just been established via HTTP. + */ + public void put(String uuid, GuacamoleTunnel tunnel) { + tunnelMap.put(uuid, new GuacamoleHTTPTunnel(tunnel)); + } + + /** + * Removes the GuacamoleTunnel having the given UUID, if such a tunnel + * exists. The original tunnel is returned wrapped within a + * GuacamoleHTTPTunnel. + * + * @param uuid + * The UUID of the tunnel to remove (deregister). + * + * @return + * The GuacamoleTunnel having the given UUID, if such a tunnel exists, + * wrapped within a GuacamoleHTTPTunnel, or null if no such tunnel + * exists and no removal was performed. + */ + public GuacamoleHTTPTunnel remove(String uuid) { + return tunnelMap.remove(uuid); + } + + /** + * Shuts down this tunnel map, disallowing future tunnels from being + * registered and reclaiming any resources. + */ + public void shutdown() { + executor.shutdownNow(); + } + +} From e1a256b1c10e54f8846fd5d07b963ef4447ade62 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 10 Dec 2015 17:53:09 -0800 Subject: [PATCH 3/3] GUAC-1427: Migrate HTTP tunnel storage to GuacamoleHTTPTunnelMap. --- .../guacamole/servlet/GuacamoleHTTPTunnelServlet.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelServlet.java b/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelServlet.java index 3ff5119cb..6dc6fc05f 100644 --- a/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelServlet.java +++ b/guacamole-common/src/main/java/org/glyptodon/guacamole/servlet/GuacamoleHTTPTunnelServlet.java @@ -28,8 +28,6 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; @@ -62,8 +60,7 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { /** * Map of absolutely all active tunnels using HTTP, indexed by tunnel UUID. */ - private final ConcurrentMap tunnels = - new ConcurrentHashMap(); + private final GuacamoleHTTPTunnelMap tunnels = new GuacamoleHTTPTunnelMap(); /** * The prefix of the query string which denotes a tunnel read operation. @@ -512,6 +509,11 @@ public abstract class GuacamoleHTTPTunnelServlet extends HttpServlet { } + @Override + public void destroy() { + tunnels.shutdown(); + } + } /**