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 69340c808..1b761d614 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,7 +26,10 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.basic.properties.BasicGuacamoleProperties; @@ -60,6 +63,11 @@ public class GuacamoleSession { */ private final ClipboardState clipboardState = new ClipboardState(); + /** + * All currently-active tunnels, indexed by tunnel UUID. + */ + private final Map tunnels = new ConcurrentHashMap(); + /** * Creates a new Guacamole session associated with the given user context. * @@ -157,5 +165,49 @@ public class GuacamoleSession { public Collection getListeners() { return Collections.unmodifiableCollection(listeners); } - + + /** + * Returns whether this session has any associated active tunnels. + * + * @return true if this session has any associated active tunnels, + * false otherwise. + */ + public boolean hasTunnels() { + return !tunnels.isEmpty(); + } + + /** + * Returns a map of all active tunnels associated with this session, where + * each key is the String representation of the tunnel's UUID. Changes to + * this map immediately affect the set of tunnels associated with this + * session. A tunnel need not be present here to be used by the user + * associated with this session, but tunnels not in this set will not + * be taken into account when determining whether a session is in use. + * + * @return A map of all active tunnels associated with this session. + */ + public Map getTunnels() { + return tunnels; + } + + /** + * Associates the given tunnel with this session, such that it is taken + * into account when determining session activity. + * + * @param tunnel The tunnel to associate with this session. + */ + public void addTunnel(GuacamoleTunnel tunnel) { + tunnels.put(tunnel.getUUID().toString(), tunnel); + } + + /** + * Disassociates the tunnel having the given UUID from this session. + * + * @param uuid The UUID of the tunnel to disassociate from this session. + * @return true if the tunnel existed and was removed, false otherwise. + */ + public boolean removeTunnel(String uuid) { + return tunnels.remove(uuid) != null; + } + } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/SessionKeepAlive.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/SessionKeepAlive.java deleted file mode 100644 index b065e4730..000000000 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/SessionKeepAlive.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright (C) 2014 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.net.basic; - -import com.google.inject.Inject; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.QueryParam; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.net.auth.UserContext; -import org.glyptodon.guacamole.net.basic.rest.AuthProviderRESTExposure; -import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * REST service which updates the last access time of the Guacamole session, - * preventing the session from becoming invalid. - * - * @author Michael Jumper - */ -@Path("/keep-alive") -public class SessionKeepAlive { - - /** - * A service for authenticating users from auth tokens. - */ - @Inject - private AuthenticationService authenticationService; - - /** - * Logger for this class. - */ - private final Logger logger = LoggerFactory.getLogger(SessionKeepAlive.class); - - @GET - @AuthProviderRESTExposure - public void updateSession(@QueryParam("token") String authToken) throws GuacamoleException { - - // Tickle the session - UserContext context = authenticationService.getUserContext(authToken); - - // Do nothing - logger.debug("Keep-alive signal received from user \"{}\".", context.self().getUsername()); - - } - -} 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 5df50b850..5c23b0455 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 @@ -25,7 +25,6 @@ package org.glyptodon.guacamole.net.basic; import com.google.inject.Inject; import com.google.inject.Singleton; import org.glyptodon.guacamole.net.basic.rest.clipboard.ClipboardRESTService; -import java.util.Collection; import java.util.List; import org.glyptodon.guacamole.GuacamoleClientException; import org.glyptodon.guacamole.GuacamoleException; @@ -35,7 +34,6 @@ import org.glyptodon.guacamole.net.GuacamoleSocket; import org.glyptodon.guacamole.net.GuacamoleTunnel; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; -import org.glyptodon.guacamole.net.auth.Credentials; import org.glyptodon.guacamole.net.auth.Directory; import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.net.basic.rest.auth.AuthenticationService; @@ -72,12 +70,10 @@ public class TunnelRequestService { private AuthenticationService authenticationService; /** - * Notifies all listeners in the given collection that a tunnel has been + * Notifies all listeners in the given session that a tunnel has been * connected. * - * @param listeners A collection of all listeners that should be notified. - * @param context The UserContext associated with the current session. - * @param credentials The credentials associated with the current session. + * @param session The session associated with the listeners to be notified. * @param tunnel The tunnel being connected. * @return true if all listeners are allowing the tunnel to connect, * or if there are no listeners, and false if any listener is @@ -88,16 +84,17 @@ public class TunnelRequestService { * error, the connect is canceled, and no other * listeners will run. */ - private boolean notifyConnect(Collection listeners, UserContext context, - Credentials credentials, GuacamoleTunnel tunnel) + private boolean notifyConnect(GuacamoleSession session, GuacamoleTunnel tunnel) throws GuacamoleException { // Build event for auth success - TunnelConnectEvent event = new TunnelConnectEvent(context, - credentials, tunnel); + TunnelConnectEvent event = new TunnelConnectEvent( + session.getUserContext(), + session.getCredentials(), + tunnel); // Notify all listeners - for (Object listener : listeners) { + for (Object listener : session.getListeners()) { if (listener instanceof TunnelConnectListener) { // Cancel immediately if hook returns false @@ -112,12 +109,10 @@ public class TunnelRequestService { } /** - * Notifies all listeners in the given collection that a tunnel has been + * Notifies all listeners in the given session that a tunnel has been * closed. * - * @param listeners A collection of all listeners that should be notified. - * @param context The UserContext associated with the current session. - * @param credentials The credentials associated with the current session. + * @param session The session associated with the listeners to be notified. * @param tunnel The tunnel being closed. * @return true if all listeners are allowing the tunnel to close, * or if there are no listeners, and false if any listener is @@ -128,16 +123,17 @@ public class TunnelRequestService { * error, the close is canceled, and no other * listeners will run. */ - private boolean notifyClose(Collection listeners, UserContext context, - Credentials credentials, GuacamoleTunnel tunnel) + private boolean notifyClose(GuacamoleSession session, GuacamoleTunnel tunnel) throws GuacamoleException { // Build event for auth success - TunnelCloseEvent event = new TunnelCloseEvent(context, - credentials, tunnel); + TunnelCloseEvent event = new TunnelCloseEvent( + session.getUserContext(), + session.getCredentials(), + tunnel); // Notify all listeners - for (Object listener : listeners) { + for (Object listener : session.getListeners()) { if (listener instanceof TunnelCloseListener) { // Cancel immediately if hook returns false @@ -165,8 +161,8 @@ public class TunnelRequestService { // Get auth token and session String authToken = request.getParameter("authToken"); - GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); - + final GuacamoleSession session = authenticationService.getGuacamoleSession(authToken); + // Get ID of connection String id = request.getParameter("id"); TunnelRequest.IdentifierType id_type = TunnelRequest.IdentifierType.getType(id); @@ -178,18 +174,6 @@ public class TunnelRequestService { // Remove prefix id = id.substring(id_type.PREFIX.length()); - // Get session-specific elements - final Credentials credentials = session.getCredentials(); - final UserContext context = session.getUserContext(); - final Collection listeners = session.getListeners(); - - // If no context or no credentials, not logged in - if (context == null || credentials == null) - throw new GuacamoleSecurityException("Cannot connect - user not logged in."); - - // Get clipboard - final ClipboardState clipboard = session.getClipboardState(); - // Get client information GuacamoleClientInformation info = new GuacamoleClientInformation(); @@ -225,6 +209,8 @@ public class TunnelRequestService { // Connection identifiers case CONNECTION: { + UserContext context = session.getUserContext(); + // Get connection directory Directory directory = context.getRootConnectionGroup().getConnectionDirectory(); @@ -245,6 +231,8 @@ public class TunnelRequestService { // Connection group identifiers case CONNECTION_GROUP: { + UserContext context = session.getUserContext(); + // Get connection group directory Directory directory = context.getRootConnectionGroup().getConnectionGroupDirectory(); @@ -271,6 +259,11 @@ 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() { @@ -293,9 +286,11 @@ public class TunnelRequestService { public void close() throws GuacamoleException { // Only close if not canceled - if (!notifyClose(listeners, context, credentials, this)) + if (!notifyClose(session, this)) throw new GuacamoleException("Tunnel close canceled by listener."); + session.removeTunnel(getUUID().toString()); + // Close if no exception due to listener super.close(); @@ -304,7 +299,7 @@ public class TunnelRequestService { }; // Notify listeners about connection - if (!notifyConnect(listeners, context, credentials, tunnel)) { + if (!notifyConnect(session, tunnel)) { logger.info("Successful connection canceled by hook."); return null; } diff --git a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java index e7815c603..6394b22ce 100644 --- a/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java +++ b/guacamole/src/main/java/org/glyptodon/guacamole/net/basic/rest/RESTServletModule.java @@ -26,7 +26,6 @@ import com.google.inject.Scopes; import com.google.inject.servlet.ServletModule; import com.sun.jersey.guice.spi.container.servlet.GuiceContainer; import org.codehaus.jackson.jaxrs.JacksonJsonProvider; -import org.glyptodon.guacamole.net.basic.SessionKeepAlive; import org.glyptodon.guacamole.net.basic.rest.auth.LoginRESTService; import org.glyptodon.guacamole.net.basic.rest.clipboard.ClipboardRESTService; import org.glyptodon.guacamole.net.basic.rest.connection.ConnectionRESTService; @@ -51,7 +50,6 @@ public class RESTServletModule extends ServletModule { bind(ConnectionGroupRESTService.class); bind(PermissionRESTService.class); bind(ProtocolRESTService.class); - bind(SessionKeepAlive.class); bind(UserRESTService.class); bind(LoginRESTService.class); 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 7b5a3e608..331d9f3ff 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 @@ -104,8 +104,16 @@ public class BasicTokenSessionMap implements TokenSessionMap { * @param authToken The auth token for the session. * @return True if the session has timed out, false otherwise. */ - private boolean sessionHasTimedOut(String authToken) { + private boolean isSessionActive(String authToken) { + GuacamoleSession session = sessionMap.get(authToken); + if (session == null) + return false; + + // A session is active if it has any active tunnels + if (session.hasTunnels()) + return true; + if (!lastAccessTimeMap.containsKey(authToken)) return true; @@ -120,7 +128,7 @@ public class BasicTokenSessionMap implements TokenSessionMap { public GuacamoleSession get(String authToken) { // If the session has timed out, evict the token and force the user to log in again - if (sessionHasTimedOut(authToken)) { + if (isSessionActive(authToken)) { evict(authToken); return null; } diff --git a/guacamole/src/main/webapp/scripts/client-ui.js b/guacamole/src/main/webapp/scripts/client-ui.js index 78649c582..65117a97a 100644 --- a/guacamole/src/main/webapp/scripts/client-ui.js +++ b/guacamole/src/main/webapp/scripts/client-ui.js @@ -1199,11 +1199,6 @@ GuacUI.Client.connect = function() { connect_string += "&video=" + encodeURIComponent(mimetype); }); - // Ping server occasionally to keep HTTP session alive - var session_keep_alive = window.setInterval(function _session_keep_alive() { - GuacamoleService.KeepAlive.ping(); - }, GuacUI.Client.KEEP_ALIVE_INTERVAL); - // Show connection errors from tunnel tunnel.onerror = function(status) { var message = GuacUI.Client.tunnel_errors[status.code] || GuacUI.Client.tunnel_errors.DEFAULT; @@ -1216,13 +1211,11 @@ GuacUI.Client.connect = function() { // Handle disconnect if (state === Guacamole.Tunnel.State.CLOSED) { - // No need for a keep-alive ping if the tunnel is closed - window.clearInterval(session_keep_alive); - // Notify of disconnections (if not already notified of something else) if (!GuacUI.Client.visibleStatus) GuacUI.Client.showStatus("Disconnected", "You have been disconnected. Reload the page to reconnect."); + } }; diff --git a/guacamole/src/main/webapp/scripts/service.js b/guacamole/src/main/webapp/scripts/service.js index 500e6abe4..a5ea8ab77 100644 --- a/guacamole/src/main/webapp/scripts/service.js +++ b/guacamole/src/main/webapp/scripts/service.js @@ -1056,29 +1056,6 @@ GuacamoleService.Clipboard = { }; -/** - * Collection of service functions which deal with the session keep-alive. Each - * function makes an explicit HTTP query to the server. In the case of the - * keep-alive ping, no response is expected, and any received response is - * ignored. - */ -GuacamoleService.KeepAlive = { - - "ping" : function() { - - // Construct request URL - var ping_url = "api/keep-alive" - + "?token=" + GuacamoleService.Auth.current().authToken; - - // Send keep-alive "ping" - var xhr = new XMLHttpRequest(); - xhr.open("GET", ping_url, true); - xhr.send(null); - - } - -}; - /** * Collection of service functions which deal with authentication. Note that, * unlike everything else here, not all functions in GuacamoleService.Auth