From a2b4b62d9f6cd1c203891b4b815743b1139f0c35 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 5 Mar 2015 14:04:34 -0800 Subject: [PATCH 1/4] GUAC-1105: Fix balancing policy semantics. --- .../AbstractGuacamoleSocketService.java | 57 +++++- .../BalancedGuacamoleSocketService.java | 88 +++++++++ .../MultiseatGuacamoleSocketService.java | 91 +++------ .../ReservedGuacamoleSocketService.java | 183 ------------------ .../guacamole/auth/jdbc/socket/Seat.java | 89 +++++++++ .../SingleSeatGuacamoleSocketService.java | 30 ++- .../UnrestrictedGuacamoleSocketService.java | 13 ++ .../mysql/MySQLAuthenticationProvider.java | 4 +- 8 files changed, 300 insertions(+), 255 deletions(-) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/BalancedGuacamoleSocketService.java delete mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/Seat.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java index 084823684..08da2decc 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java @@ -139,6 +139,37 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS protected abstract void release(AuthenticatedUser user, ModeledConnection connection); + /** + * Acquires possibly-exclusive access to the given connection group on + * behalf of the given user. If access is denied for any reason, an + * exception is thrown. + * + * @param user + * The user acquiring access. + * + * @param connectionGroup + * The connection group being accessed. + * + * @throws GuacamoleException + * If access is denied to the given user for any reason. + */ + protected abstract void acquire(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) throws GuacamoleException; + + /** + * Releases possibly-exclusive access to the given connection group on + * behalf of the given user. If the given user did not already have access, + * the behavior of this function is undefined. + * + * @param user + * The user releasing access. + * + * @param connectionGroup + * The connection group being released. + */ + protected abstract void release(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup); + /** * Returns a guacamole configuration containing the protocol and parameters * from the given connection. If tokens are used in the connection @@ -254,6 +285,11 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS * @param user * The user for whom the connection is being established. * + * @param balancingGroup, + * The associated balancing group, if any. If the connection is not + * associated with a balancing group, or the connection is being used + * manually, this will be null. + * * @param connection * The connection the user is connecting to. * @@ -270,6 +306,7 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS * while connection configuration information is being retrieved. */ private GuacamoleSocket connect(final AuthenticatedUser user, + final ModeledConnectionGroup balancingGroup, final ModeledConnection connection, GuacamoleClientInformation info) throws GuacamoleException { @@ -309,6 +346,10 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS activeConnectionGroups.remove(parentIdentifier, activeConnection); release(user, connection); + // Release any associated group + if (balancingGroup != null) + release(user, balancingGroup); + // Save record to database saveConnectionRecord(identifier, activeConnection); @@ -325,9 +366,16 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS // Release connection if not already released if (released.compareAndSet(false, true)) { + + // Release connection activeConnections.remove(identifier, activeConnection); activeConnectionGroups.remove(parentIdentifier, activeConnection); release(user, connection); + + // Release any associated group + if (balancingGroup != null) + release(user, balancingGroup); + } throw e; @@ -344,7 +392,7 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS // Acquire and connect to single connection acquire(user, Collections.singletonList(connection)); - return connect(user, connection, info); + return connect(user, null, connection, info); } @@ -368,7 +416,10 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS if (identifiers.isEmpty()) throw new GuacamoleSecurityException("Permission denied."); - // Otherwise, retrieve all children + // Acquire group + acquire(user, connectionGroup); + + // Retrieve all children Collection models = connectionMapper.select(identifiers); List connections = new ArrayList(models.size()); @@ -381,7 +432,7 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS // Acquire and connect to any child ModeledConnection connection = acquire(user, connections); - return connect(user, connection, info); + return connect(user, connectionGroup, connection, info); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/BalancedGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/BalancedGuacamoleSocketService.java new file mode 100644 index 000000000..118c0cad9 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/BalancedGuacamoleSocketService.java @@ -0,0 +1,88 @@ +/* + * 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.auth.jdbc.socket; + +import com.google.inject.Singleton; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; +import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.GuacamoleResourceConflictException; +import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; + + +/** + * GuacamoleSocketService implementation which allows only one user per + * connection at any time, but does not disallow concurrent use of connection + * groups. If a user attempts to use a connection group multiple times, they + * will receive different underlying connections each time until the group is + * exhausted. + * + * @author Michael Jumper + */ +@Singleton +public class BalancedGuacamoleSocketService + extends AbstractGuacamoleSocketService { + + /** + * The set of all active connection identifiers. + */ + private final Set activeConnections = + Collections.newSetFromMap(new ConcurrentHashMap()); + + @Override + protected ModeledConnection acquire(AuthenticatedUser user, + List connections) throws GuacamoleException { + + // Return the first unused connection + for (ModeledConnection connection : connections) { + if (activeConnections.add(connection.getIdentifier())) + return connection; + } + + // Already in use + throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use."); + + } + + @Override + protected void release(AuthenticatedUser user, ModeledConnection connection) { + activeConnections.remove(connection.getIdentifier()); + } + + @Override + protected void acquire(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) throws GuacamoleException { + // Do nothing + } + + @Override + protected void release(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) { + // Do nothing + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java index a55e31780..d53408c73 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java @@ -31,12 +31,13 @@ import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceConflictException; +import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; /** * GuacamoleSocketService implementation which restricts concurrency only on a - * per-user basis. Each connection may be used concurrently any number of - * times, but each concurrent use must be associated with a different user. + * per-user basis. Each connection or group may be used concurrently any number + * of times, but each concurrent use must be associated with a different user. * * @author Michael Jumper */ @@ -44,74 +45,17 @@ import org.glyptodon.guacamole.GuacamoleResourceConflictException; public class MultiseatGuacamoleSocketService extends AbstractGuacamoleSocketService { - /** - * A unique pairing of user and connection. - */ - private static class Seat { - - /** - * The user using this seat. - */ - private final String username; - - /** - * The connection associated with this seat. - */ - private final String connectionIdentifier; - - /** - * Creates a new seat which associated the given user with the given - * connection. - * - * @param username - * The username of the user using this seat. - * - * @param connectionIdentifier - * The identifier of the connection associated with this seat. - */ - public Seat(String username, String connectionIdentifier) { - this.username = username; - this.connectionIdentifier = connectionIdentifier; - } - - @Override - public int hashCode() { - - // The various properties will never be null - assert(username != null); - assert(connectionIdentifier != null); - - // Derive hashcode from username and connection identifier - int hash = 5; - hash = 37 * hash + username.hashCode(); - hash = 37 * hash + connectionIdentifier.hashCode(); - return hash; - - } - - @Override - public boolean equals(Object object) { - - // We are only comparing against other seats here - assert(object instanceof Seat); - Seat seat = (Seat) object; - - // The various properties will never be null - assert(seat.username != null); - assert(seat.connectionIdentifier != null); - - return username.equals(seat.username) - && connectionIdentifier.equals(seat.connectionIdentifier); - - } - - } - /** * The set of all active user/connection pairs. */ private final Set activeSeats = Collections.newSetFromMap(new ConcurrentHashMap()); + + /** + * The set of all active user/connection group pairs. + */ + private final Set activeGroupSeats = + Collections.newSetFromMap(new ConcurrentHashMap()); @Override protected ModeledConnection acquire(AuthenticatedUser user, @@ -135,4 +79,21 @@ public class MultiseatGuacamoleSocketService activeSeats.remove(new Seat(user.getUser().getIdentifier(), connection.getIdentifier())); } + @Override + protected void acquire(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) throws GuacamoleException { + + // Do not allow duplicate use of connection groups + Seat seat = new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier()); + if (!activeGroupSeats.add(seat)) + throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use."); + + } + + @Override + protected void release(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) { + activeGroupSeats.remove(new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier())); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java deleted file mode 100644 index 8cab0b814..000000000 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java +++ /dev/null @@ -1,183 +0,0 @@ -/* - * 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.auth.jdbc.socket; - -import com.google.inject.Singleton; -import java.util.List; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; -import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; -import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; -import org.glyptodon.guacamole.GuacamoleException; -import org.glyptodon.guacamole.GuacamoleResourceConflictException; - - -/** - * GuacamoleSocketService implementation which allows only one user per - * connection at any time, but does not disallow concurrent use. Once - * connected, a user has effectively reserved that connection, and may - * continue to concurrently use that connection any number of times. The - * connection will remain reserved until all associated connections are closed. - * Other users will be denied access to that connection while it is reserved. - * - * @author Michael Jumper - */ -@Singleton -public class ReservedGuacamoleSocketService - extends AbstractGuacamoleSocketService { - - /** - * An arbitrary number of reservations associated with a specific user. - * Initially, each Reservation instance represents exactly one reservation, - * but future calls to acquire() may increase this value. Once the - * reservation count is reduced to zero by calls to release(), a - * Reservation instance is empty and cannot be reused. It must be discarded - * and replaced with a fresh Reservation. - * - * This is necessary as each Reservation will be stored within a Map, and - * the effect of acquire() must be deterministic. If Reservations could be - * reused, the internal count could potentially increase after being - * removed from the map, resulting in a successful acquire() that really - * should have failed. - */ - private static class Reservation { - - /** - * The username of the user associated with this reservation. - */ - private final String username; - - /** - * The number of reservations effectively present under the associated - * username. - */ - private int count = 1; - - /** - * Creates a new reservation which tracks the overall number of - * reservations for a given user. - * @param username - */ - public Reservation(String username) { - this.username = username; - } - - /** - * Attempts to acquire a new reservation under the given username. If - * this reservation is for a different user, or the reservation has - * expired, this will fail. - * - * @param username - * The username of the user to acquire the reservation for. - * - * @return - * true if the reservation was successful, false otherwise. - */ - public boolean acquire(String username) { - - // Acquire always fails if for the wrong user - if (!this.username.equals(username)) - return false; - - // Determine success/failure based on count - synchronized (this) { - - // If already expired, no further reservations are allowed - if (count == 0) - return false; - - // Otherwise, add another reservation, report success - count++; - return true; - - } - - } - - /** - * Releases a previous reservation. The result of calling this function - * without a previous matching call to acquire is undefined. - * - * @return - * true if the last reservation has been released and this - * reservation is now empty, false otherwise. - */ - public boolean release() { - synchronized (this) { - - // Reduce reservation count - count--; - - // Empty if no reservations remain - return count == 0; - - } - } - - } - - /** - * Map of connection identifier to associated reservations. - */ - private final ConcurrentMap reservations = - new ConcurrentHashMap(); - - @Override - protected ModeledConnection acquire(AuthenticatedUser user, - List connections) throws GuacamoleException { - - String username = user.getUser().getIdentifier(); - - // Return the first successfully-reserved connection - for (ModeledConnection connection : connections) { - - String identifier = connection.getIdentifier(); - - // Attempt to reserve connection, return if successful - Reservation reservation = reservations.putIfAbsent(identifier, new Reservation(username)); - if (reservation == null || reservation.acquire(username)) - return connection; - - } - - // Already in use - throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use."); - - } - - @Override - protected void release(AuthenticatedUser user, ModeledConnection connection) { - - String identifier = connection.getIdentifier(); - - // Retrieve active reservation (which must exist) - Reservation reservation = reservations.get(identifier); - assert(reservation != null); - - // Release reservation, remove from map if empty - if (reservation.release()) - reservations.remove(identifier); - - } - -} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/Seat.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/Seat.java new file mode 100644 index 000000000..275536f98 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/Seat.java @@ -0,0 +1,89 @@ +/* + * 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.auth.jdbc.socket; + +/** + * A unique pairing of user and connection or connection group. + * + * @author Michael Jumper + */ +public class Seat { + + /** + * The user using this seat. + */ + private final String username; + + /** + * The connection or connection group associated with this seat. + */ + private final String identifier; + + /** + * Creates a new seat which associated the given user with the given + * connection or connection group. + * + * @param username + * The username of the user using this seat. + * + * @param identifier + * The identifier of the connection or connection group associated with + * this seat. + */ + public Seat(String username, String identifier) { + this.username = username; + this.identifier = identifier; + } + + @Override + public int hashCode() { + + // The various properties will never be null + assert(username != null); + assert(identifier != null); + + // Derive hashcode from username and connection identifier + int hash = 5; + hash = 37 * hash + username.hashCode(); + hash = 37 * hash + identifier.hashCode(); + return hash; + + } + + @Override + public boolean equals(Object object) { + + // We are only comparing against other seats here + assert(object instanceof Seat); + Seat seat = (Seat) object; + + // The various properties will never be null + assert(seat.username != null); + assert(seat.identifier != null); + + return username.equals(seat.username) + && identifier.equals(seat.identifier); + + } + +} diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java index e3ff6a9a4..b7f092565 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java @@ -31,11 +31,14 @@ import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.GuacamoleException; import org.glyptodon.guacamole.GuacamoleResourceConflictException; +import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; /** * GuacamoleSocketService implementation which allows exactly one use - * of any connection at any time. Concurrent usage of any kind is not allowed. + * of any connection at any time. Concurrent usage of connections is not + * allowed, and concurrent usage of connection groups is allowed only between + * different users. * * @author Michael Jumper */ @@ -48,7 +51,13 @@ public class SingleSeatGuacamoleSocketService */ private final Set activeConnections = Collections.newSetFromMap(new ConcurrentHashMap()); - + + /** + * The set of all active user/connection group pairs. + */ + private final Set activeGroupSeats = + Collections.newSetFromMap(new ConcurrentHashMap()); + @Override protected ModeledConnection acquire(AuthenticatedUser user, List connections) throws GuacamoleException { @@ -69,4 +78,21 @@ public class SingleSeatGuacamoleSocketService activeConnections.remove(connection.getIdentifier()); } + @Override + protected void acquire(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) throws GuacamoleException { + + // Do not allow duplicate use of connection groups + Seat seat = new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier()); + if (!activeGroupSeats.add(seat)) + throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use."); + + } + + @Override + protected void release(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) { + activeGroupSeats.remove(new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier())); + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/UnrestrictedGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/UnrestrictedGuacamoleSocketService.java index c5d2a0503..e5932d8c2 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/UnrestrictedGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/UnrestrictedGuacamoleSocketService.java @@ -27,6 +27,7 @@ import java.util.List; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; /** @@ -66,4 +67,16 @@ public class UnrestrictedGuacamoleSocketService // Do nothing } + @Override + protected void acquire(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) throws GuacamoleException { + // Do nothing + } + + @Override + protected void release(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) { + // Do nothing + } + } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLAuthenticationProvider.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLAuthenticationProvider.java index c33787a38..3e7463b69 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLAuthenticationProvider.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-mysql/src/main/java/net/sourceforge/guacamole/net/auth/mysql/MySQLAuthenticationProvider.java @@ -31,7 +31,7 @@ import org.glyptodon.guacamole.net.auth.UserContext; import org.glyptodon.guacamole.auth.jdbc.JDBCAuthenticationProviderModule; import org.glyptodon.guacamole.auth.jdbc.socket.GuacamoleSocketService; import org.glyptodon.guacamole.auth.jdbc.socket.MultiseatGuacamoleSocketService; -import org.glyptodon.guacamole.auth.jdbc.socket.ReservedGuacamoleSocketService; +import org.glyptodon.guacamole.auth.jdbc.socket.BalancedGuacamoleSocketService; import org.glyptodon.guacamole.auth.jdbc.socket.SingleSeatGuacamoleSocketService; import org.glyptodon.guacamole.auth.jdbc.socket.UnrestrictedGuacamoleSocketService; import org.glyptodon.guacamole.auth.jdbc.user.UserContextService; @@ -85,7 +85,7 @@ public class MySQLAuthenticationProvider implements AuthenticationProvider { // Connections are reserved for a single user when in use else - return ReservedGuacamoleSocketService.class; + return BalancedGuacamoleSocketService.class; } From 331772865804188f8d8a195f5a1909c32c4493a0 Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 5 Mar 2015 15:21:56 -0800 Subject: [PATCH 2/4] GUAC-1105: Find first available, least-used seat in multiseat policy. --- .../MultiseatGuacamoleSocketService.java | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java index d53408c73..aa1786f6f 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java @@ -23,7 +23,9 @@ package org.glyptodon.guacamole.auth.jdbc.socket; import com.google.inject.Singleton; +import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; @@ -62,9 +64,23 @@ public class MultiseatGuacamoleSocketService List connections) throws GuacamoleException { String username = user.getUser().getIdentifier(); + + // Sort connections in ascending order of usage + ModeledConnection[] sortedConnections = connections.toArray(new ModeledConnection[connections.size()]); + Arrays.sort(sortedConnections, new Comparator() { + + @Override + public int compare(ModeledConnection a, ModeledConnection b) { + + return getActiveConnections(a).size() + - getActiveConnections(b).size(); + + } + + }); // Return the first unreserved connection - for (ModeledConnection connection : connections) { + for (ModeledConnection connection : sortedConnections) { if (activeSeats.add(new Seat(username, connection.getIdentifier()))) return connection; } From 31661144301c86246cf9f9f2446b603911525c3d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 5 Mar 2015 15:25:42 -0800 Subject: [PATCH 3/4] GUAC-1105: Throw GuacamoleClientTooManyException if connection group usage is denied due to duplicate use. --- .../auth/jdbc/socket/MultiseatGuacamoleSocketService.java | 3 ++- .../auth/jdbc/socket/SingleSeatGuacamoleSocketService.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java index aa1786f6f..867c1b5d0 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java @@ -29,6 +29,7 @@ import java.util.Comparator; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.glyptodon.guacamole.GuacamoleClientTooManyException; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.GuacamoleException; @@ -102,7 +103,7 @@ public class MultiseatGuacamoleSocketService // Do not allow duplicate use of connection groups Seat seat = new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier()); if (!activeGroupSeats.add(seat)) - throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use."); + throw new GuacamoleClientTooManyException("Cannot connect. Connection group already in use by this user."); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java index b7f092565..383e85f66 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java @@ -27,6 +27,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.glyptodon.guacamole.GuacamoleClientTooManyException; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.GuacamoleException; @@ -85,7 +86,7 @@ public class SingleSeatGuacamoleSocketService // Do not allow duplicate use of connection groups Seat seat = new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier()); if (!activeGroupSeats.add(seat)) - throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use."); + throw new GuacamoleClientTooManyException("Cannot connect. Connection group already in use by this user."); } From 9d6828bf3a8fc04d9543a626c6813d2195151c9d Mon Sep 17 00:00:00 2001 From: Michael Jumper Date: Thu, 5 Mar 2015 16:36:50 -0800 Subject: [PATCH 4/4] GUAC-1105: Reduce code complexity of AbstractGuacamoleSocketService. --- .../AbstractGuacamoleSocketService.java | 243 ++++++++++-------- .../jdbc/socket/ActiveConnectionRecord.java | 83 +++++- .../socket/ManagedInetGuacamoleSocket.java | 71 +++++ 3 files changed, 289 insertions(+), 108 deletions(-) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ManagedInetGuacamoleSocket.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java index 08da2decc..7647ff54d 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/AbstractGuacamoleSocketService.java @@ -44,7 +44,6 @@ import org.glyptodon.guacamole.GuacamoleSecurityException; import org.glyptodon.guacamole.auth.jdbc.connection.ConnectionMapper; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.net.GuacamoleSocket; -import org.glyptodon.guacamole.net.InetGuacamoleSocket; import org.glyptodon.guacamole.net.auth.Connection; import org.glyptodon.guacamole.net.auth.ConnectionGroup; import org.glyptodon.guacamole.net.auth.ConnectionRecord; @@ -214,19 +213,17 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS } /** - * Saves the given ActiveConnectionRecord to the database, associating it - * with the connection having the given identifier. The end date of the - * saved record will be populated with the current time. - * - * @param identifier - * The connection to associate the new record with. + * Saves the given ActiveConnectionRecord to the database. The end date of + * the saved record will be populated with the current time. * * @param record * The record to save. */ - private void saveConnectionRecord(String identifier, - ActiveConnectionRecord record) { + private void saveConnectionRecord(ActiveConnectionRecord record) { + // Get associated connection + ModeledConnection connection = record.getConnection(); + // Get associated models AuthenticatedUser user = record.getUser(); UserModel userModel = user.getUser().getModel(); @@ -235,7 +232,7 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS // Copy user information and timestamps into new record recordModel.setUserID(userModel.getObjectID()); recordModel.setUsername(userModel.getIdentifier()); - recordModel.setConnectionIdentifier(identifier); + recordModel.setConnectionIdentifier(connection.getIdentifier()); recordModel.setStartDate(record.getStartDate()); recordModel.setEndDate(new Date()); @@ -255,24 +252,88 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS * If an error occurs while connecting to guacd, or while parsing * guacd-related properties. */ - private GuacamoleSocket getUnconfiguredGuacamoleSocket() + private GuacamoleSocket getUnconfiguredGuacamoleSocket(Runnable socketClosedCallback) throws GuacamoleException { // Use SSL if requested if (environment.getProperty(Environment.GUACD_SSL, true)) - return new InetGuacamoleSocket( + return new ManagedInetGuacamoleSocket( environment.getRequiredProperty(Environment.GUACD_HOSTNAME), - environment.getRequiredProperty(Environment.GUACD_PORT) + environment.getRequiredProperty(Environment.GUACD_PORT), + socketClosedCallback ); // Otherwise, just use straight TCP - return new InetGuacamoleSocket( + return new ManagedInetGuacamoleSocket( environment.getRequiredProperty(Environment.GUACD_HOSTNAME), - environment.getRequiredProperty(Environment.GUACD_PORT) + environment.getRequiredProperty(Environment.GUACD_PORT), + socketClosedCallback ); } - + + /** + * Task which handles cleanup of a connection associated with some given + * ActiveConnectionRecord. + */ + private class ConnectionCleanupTask implements Runnable { + + /** + * Whether this task has run. + */ + private final AtomicBoolean hasRun = new AtomicBoolean(false); + + /** + * The ActiveConnectionRecord whose connection will be cleaned up once + * this task runs. + */ + private final ActiveConnectionRecord activeConnection; + + /** + * Creates a new task which automatically cleans up after the + * connection associated with the given ActiveConnectionRecord. The + * connection and parent group will be removed from the maps of active + * connections and groups, and exclusive access will be released. + * + * @param activeConnection + * The ActiveConnectionRecord whose associated connection should be + * cleaned up once this task runs. + */ + public ConnectionCleanupTask(ActiveConnectionRecord activeConnection) { + this.activeConnection = activeConnection; + } + + @Override + public void run() { + + // Only run once + if (!hasRun.compareAndSet(false, true)) + return; + + // Get original user and connection + AuthenticatedUser user = activeConnection.getUser(); + ModeledConnection connection = activeConnection.getConnection(); + + // Get associated identifiers + String identifier = connection.getIdentifier(); + String parentIdentifier = connection.getParentIdentifier(); + + // Release connection + activeConnections.remove(identifier, activeConnection); + activeConnectionGroups.remove(parentIdentifier, activeConnection); + release(user, connection); + + // Release any associated group + if (activeConnection.hasBalancingGroup()) + release(user, activeConnection.getBalancingGroup()); + + // Save history record to database + saveConnectionRecord(activeConnection); + + } + + } + /** * Creates a socket for the given user which connects to the given * connection, which MUST already be acquired via acquire(). The given @@ -285,11 +346,6 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS * @param user * The user for whom the connection is being established. * - * @param balancingGroup, - * The associated balancing group, if any. If the connection is not - * associated with a balancing group, or the connection is being used - * manually, this will be null. - * * @param connection * The connection the user is connecting to. * @@ -305,85 +361,77 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS * If an error occurs while the connection is being established, or * while connection configuration information is being retrieved. */ - private GuacamoleSocket connect(final AuthenticatedUser user, - final ModeledConnectionGroup balancingGroup, - final ModeledConnection connection, GuacamoleClientInformation info) + private GuacamoleSocket getGuacamoleSocket(ActiveConnectionRecord activeConnection, + GuacamoleClientInformation info) throws GuacamoleException { - // Create record for active connection - final ActiveConnectionRecord activeConnection = new ActiveConnectionRecord(user); + ModeledConnection connection = activeConnection.getConnection(); + + // Record new active connection + Runnable cleanupTask = new ConnectionCleanupTask(activeConnection); + activeConnections.put(connection.getIdentifier(), activeConnection); + activeConnectionGroups.put(connection.getParentIdentifier(), activeConnection); - // Get relevant identifiers - final AtomicBoolean released = new AtomicBoolean(false); - final String identifier = connection.getIdentifier(); - final String parentIdentifier = connection.getParentIdentifier(); - // Return new socket try { - - // Record new active connection - activeConnections.put(identifier, activeConnection); - activeConnectionGroups.put(parentIdentifier, activeConnection); - - // Return newly-reserved connection return new ConfiguredGuacamoleSocket( - getUnconfiguredGuacamoleSocket(), - getGuacamoleConfiguration(user, connection), + getUnconfiguredGuacamoleSocket(cleanupTask), + getGuacamoleConfiguration(activeConnection.getUser(), connection), info - ) { - - @Override - public void close() throws GuacamoleException { - - // Attempt to close connection - super.close(); - - // Release connection upon close, if not already released - if (released.compareAndSet(false, true)) { - - // Release connection - activeConnections.remove(identifier, activeConnection); - activeConnectionGroups.remove(parentIdentifier, activeConnection); - release(user, connection); - - // Release any associated group - if (balancingGroup != null) - release(user, balancingGroup); - - // Save record to database - saveConnectionRecord(identifier, activeConnection); - - } - - } // end close() - - }; - + ); } - // Release connection in case of error + // Execute cleanup if socket could not be created catch (GuacamoleException e) { - - // Release connection if not already released - if (released.compareAndSet(false, true)) { - - // Release connection - activeConnections.remove(identifier, activeConnection); - activeConnectionGroups.remove(parentIdentifier, activeConnection); - release(user, connection); - - // Release any associated group - if (balancingGroup != null) - release(user, balancingGroup); - - } - + cleanupTask.run(); throw e; - } } + /** + * Returns a list of all balanced connections within a given connection + * group. If the connection group is not balancing, or it contains no + * connections, an empty list is returned. + * + * @param user + * The user on whose behalf the balanced connections within the given + * connection group are being retrieved. + * + * @param connectionGroup + * The connection group to retrieve the balanced connections of. + * + * @return + * A list containing all balanced connections within the given group, + * or an empty list if there are no such connections. + */ + private List getBalancedConnections(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) { + + // If not a balancing group, there are no balanced connections + if (connectionGroup.getType() != ConnectionGroup.Type.BALANCING) + return Collections.EMPTY_LIST; + + // If group has no children, there are no balanced connections + Collection identifiers = connectionMapper.selectIdentifiersWithin(connectionGroup.getIdentifier()); + if (identifiers.isEmpty()) + return Collections.EMPTY_LIST; + + // Retrieve all children + Collection models = connectionMapper.select(identifiers); + List connections = new ArrayList(models.size()); + + // Convert each retrieved model to a modeled connection + for (ConnectionModel model : models) { + ModeledConnection connection = connectionProvider.get(); + connection.init(user, model); + connections.add(connection); + } + + return connections; + + } + @Override @Transactional public GuacamoleSocket getGuacamoleSocket(final AuthenticatedUser user, @@ -392,7 +440,7 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS // Acquire and connect to single connection acquire(user, Collections.singletonList(connection)); - return connect(user, null, connection, info); + return getGuacamoleSocket(new ActiveConnectionRecord(user, connection), info); } @@ -407,32 +455,17 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS ModeledConnectionGroup connectionGroup, GuacamoleClientInformation info) throws GuacamoleException { - // If not a balancing group, cannot connect - if (connectionGroup.getType() != ConnectionGroup.Type.BALANCING) - throw new GuacamoleSecurityException("Permission denied."); - - // If group has no children, cannot connect - Collection identifiers = connectionMapper.selectIdentifiersWithin(connectionGroup.getIdentifier()); - if (identifiers.isEmpty()) + // If group has no associated balanced connections, cannot connect + List connections = getBalancedConnections(user, connectionGroup); + if (connections.isEmpty()) throw new GuacamoleSecurityException("Permission denied."); // Acquire group acquire(user, connectionGroup); - // Retrieve all children - Collection models = connectionMapper.select(identifiers); - List connections = new ArrayList(models.size()); - - // Convert each retrieved model to a modeled connection - for (ConnectionModel model : models) { - ModeledConnection connection = connectionProvider.get(); - connection.init(user, model); - connections.add(connection); - } - // Acquire and connect to any child ModeledConnection connection = acquire(user, connections); - return connect(user, connectionGroup, connection, info); + return getGuacamoleSocket(new ActiveConnectionRecord(user, connectionGroup, connection), info); } diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ActiveConnectionRecord.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ActiveConnectionRecord.java index e6d520b87..5d23a7443 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ActiveConnectionRecord.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ActiveConnectionRecord.java @@ -23,6 +23,8 @@ package org.glyptodon.guacamole.auth.jdbc.socket; import java.util.Date; +import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; +import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.net.auth.ConnectionRecord; @@ -43,21 +45,62 @@ public class ActiveConnectionRecord implements ConnectionRecord { */ private final AuthenticatedUser user; + /** + * The balancing group from which the associated connection was chosen, if + * any. If no balancing group was used, this will be null. + */ + private final ModeledConnectionGroup balancingGroup; + + /** + * The connection associated with this connection record. + */ + private final ModeledConnection connection; + /** * The time this connection record was created. */ private final Date startDate = new Date(); /** - * Creates a new connection record associated with the given user. The - * start date of this connection record will be the time of its creation. + * Creates a new connection record associated with the given user, + * connection, and balancing connection group. The given balancing + * connection group MUST be the connection group from which the given + * connection was chosen. The start date of this connection record will be + * the time of its creation. * * @param user * The user that connected to the connection associated with this * connection record. + * + * @param balancingGroup + * The balancing group from which the given connection was chosen. + * + * @param connection + * The connection to associate with this connection record. */ - public ActiveConnectionRecord(AuthenticatedUser user) { + public ActiveConnectionRecord(AuthenticatedUser user, + ModeledConnectionGroup balancingGroup, + ModeledConnection connection) { this.user = user; + this.balancingGroup = balancingGroup; + this.connection = connection; + } + + /** + * Creates a new connection record associated with the given user and + * connection. The start date of this connection record will be the time of + * its creation. + * + * @param user + * The user that connected to the connection associated with this + * connection record. + * + * @param connection + * The connection to associate with this connection record. + */ + public ActiveConnectionRecord(AuthenticatedUser user, + ModeledConnection connection) { + this(user, null, connection); } /** @@ -72,6 +115,40 @@ public class ActiveConnectionRecord implements ConnectionRecord { return user; } + /** + * Returns the balancing group from which the connection associated with + * this connection record was chosen. + * + * @return + * The balancing group from which the connection associated with this + * connection record was chosen. + */ + public ModeledConnectionGroup getBalancingGroup() { + return balancingGroup; + } + + /** + * Returns the connection associated with this connection record. + * + * @return + * The connection associated with this connection record. + */ + public ModeledConnection getConnection() { + return connection; + } + + /** + * Returns whether the connection associated with this connection record + * was chosen from a balancing group. + * + * @return + * true if the connection associated with this connection record was + * chosen from a balancing group, false otherwise. + */ + public boolean hasBalancingGroup() { + return balancingGroup != null; + } + @Override public Date getStartDate() { return startDate; diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ManagedInetGuacamoleSocket.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ManagedInetGuacamoleSocket.java new file mode 100644 index 000000000..8658bba4c --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ManagedInetGuacamoleSocket.java @@ -0,0 +1,71 @@ +/* + * 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.auth.jdbc.socket; + +import org.glyptodon.guacamole.GuacamoleException; +import org.glyptodon.guacamole.net.InetGuacamoleSocket; + +/** + * Implementation of GuacamoleSocket which connects via TCP to a given hostname + * and port. If the socket is closed for any reason, a given task is run. + * + * @author Michael Jumper + */ +public class ManagedInetGuacamoleSocket extends InetGuacamoleSocket { + + /** + * The task to run when the socket is closed. + */ + private final Runnable socketClosedTask; + + /** + * Creates a new socket which connects via TCP to a given hostname and + * port. If the socket is closed for any reason, the given task is run. + * + * @param hostname + * The hostname of the Guacamole proxy server to connect to. + * + * @param port + * The port of the Guacamole proxy server to connect to. + * + * @param socketClosedTask + * The task to run when the socket is closed. This task will NOT be + * run if an exception occurs during connection, and this + * ManagedInetGuacamoleSocket instance is ultimately not created. + * + * @throws GuacamoleException + * If an error occurs while connecting to the Guacamole proxy server. + */ + public ManagedInetGuacamoleSocket(String hostname, int port, + Runnable socketClosedTask) throws GuacamoleException { + super(hostname, port); + this.socketClosedTask = socketClosedTask; + } + + @Override + public void close() throws GuacamoleException { + super.close(); + socketClosedTask.run(); + } + +}