From 3b94f5d4b3c8c2e9d099c96125b66ea336e3228b Mon Sep 17 00:00:00 2001 From: James Muehlner Date: Thu, 20 Aug 2015 17:57:19 -0700 Subject: [PATCH] GUAC-830: Add ConfigurableGuacamoleTunnelService. --- .../modules/guacamole-auth-jdbc-base/pom.xml | 9 +- .../ConfigurableGuacamoleTunnelService.java | 276 ++++++++++++++++++ 2 files changed, 284 insertions(+), 1 deletion(-) create mode 100644 extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/ConfigurableGuacamoleTunnelService.java diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml index 69107ba42..1556f21da 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/pom.xml @@ -90,7 +90,14 @@ guice-multibindings 3.0 - + + + + com.google.guava + guava + 18.0 + + diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/ConfigurableGuacamoleTunnelService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/ConfigurableGuacamoleTunnelService.java new file mode 100644 index 000000000..a3f5b6d0e --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/tunnel/ConfigurableGuacamoleTunnelService.java @@ -0,0 +1,276 @@ +/* + * 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.tunnel; + +import com.google.common.collect.ConcurrentHashMultiset; +import com.google.inject.Singleton; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +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; +import org.glyptodon.guacamole.GuacamoleResourceConflictException; +import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; + + +/** + * GuacamoleTunnelService implementation which restricts concurrency for each + * connection and group according to a maximum number of connections and + * maximum number of connections per user. + * + * @author James Muehlner + * @author Michael Jumper + */ +@Singleton +public class ConfigurableGuacamoleTunnelService + extends AbstractGuacamoleTunnelService { + + /** + * Set of all currently-active user/connection pairs (seats). + */ + private final ConcurrentHashMultiset activeSeats = ConcurrentHashMultiset.create(); + + /** + * Set of all currently-active connections. + */ + private final ConcurrentHashMultiset activeConnections = ConcurrentHashMultiset.create(); + + /** + * Set of all currently-active user/connection group pairs (seats). + */ + private final ConcurrentHashMultiset activeGroupSeats = ConcurrentHashMultiset.create(); + + /** + * Set of all currently-active connection groups. + */ + private final ConcurrentHashMultiset activeGroups = ConcurrentHashMultiset.create(); + + /** + * The maximum number of connections allowed per connection by default, or + * zero if no default limit applies. + */ + private final int connectionDefaultMaxConnections; + + /** + * The maximum number of connections a user may have to any one connection + * by default, or zero if no default limit applies. + */ + private final int connectionDefaultMaxConnectionsPerUser; + + /** + * The maximum number of connections allowed per connection group by + * default, or zero if no default limit applies. + */ + private final int connectionGroupDefaultMaxConnections; + + /** + * The maximum number of connections a user may have to any one connection + * group by default, or zero if no default limit applies. + */ + private final int connectionGroupDefaultMaxConnectionsPerUser; + + /** + * Creates a new ConfigurableGuacamoleTunnelService which applies the given + * limitations when new connections are acquired. + * + * @param connectionDefaultMaxConnections + * The maximum number of connections allowed per connection by default, + * or zero if no default limit applies. + * + * @param connectionDefaultMaxConnectionsPerUser + * The maximum number of connections a user may have to any one + * connection by default, or zero if no default limit applies. + * + * @param connectionGroupDefaultMaxConnections + * The maximum number of connections allowed per connection group by + * default, or zero if no default limit applies. + * + * @param connectionGroupDefaultMaxConnectionsPerUser + * The maximum number of connections a user may have to any one + * connection group by default, or zero if no default limit applies. + */ + public ConfigurableGuacamoleTunnelService( + int connectionDefaultMaxConnections, + int connectionDefaultMaxConnectionsPerUser, + int connectionGroupDefaultMaxConnections, + int connectionGroupDefaultMaxConnectionsPerUser) { + + // Set default connection limits + this.connectionDefaultMaxConnections = connectionDefaultMaxConnections; + this.connectionDefaultMaxConnectionsPerUser = connectionDefaultMaxConnectionsPerUser; + + // Set default connection group limits + this.connectionGroupDefaultMaxConnections = connectionGroupDefaultMaxConnections; + this.connectionGroupDefaultMaxConnectionsPerUser = connectionGroupDefaultMaxConnectionsPerUser; + + } + + /** + * Attempts to add a single instance of the given value to the given + * multiset without exceeding the specified maximum number of values. If + * the value cannot be added without exceeding the maximum, false is + * returned. + * + * @param + * The type of values contained within the multiset. + * + * @param multiset + * The multiset to attempt to add a value to. + * + * @param value + * The value to attempt to add. + * + * @param max + * The maximum number of each distinct value that the given multiset + * should hold, or zero if no limit applies. + * + * @return + * true if the value was successfully added without exceeding the + * specified maximum, false if the value could not be added. + */ + private boolean tryAdd(ConcurrentHashMultiset multiset, T value, int max) { + + // Repeatedly attempt to add a new value to the given multiset until we + // explicitly succeed or explicitly fail + while (true) { + + // Get current number of values + int count = multiset.count(value); + + // Bail out if the maximum has already been reached + if (count >= max || max == 0) + return false; + + // Attempt to add one more value + if (multiset.setCount(value, count, count+1)) + return true; + + // Try again if unsuccessful + + } + + } + + @Override + protected ModeledConnection acquire(AuthenticatedUser user, + List connections) throws GuacamoleException { + + // Get username + 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(); + + } + + }); + + // Track whether acquire fails due to user-specific limits + boolean userSpecificFailure = true; + + // Return the first unreserved connection + for (ModeledConnection connection : sortedConnections) { + + // Attempt to aquire connection according to per-user limits + Seat seat = new Seat(username, connection.getIdentifier()); + if (tryAdd(activeSeats, seat, + connectionDefaultMaxConnectionsPerUser)) { + + // Attempt to aquire connection according to overall limits + if (tryAdd(activeConnections, connection.getIdentifier(), + connectionDefaultMaxConnections)) + return connection; + + // Acquire failed - retry with next connection + activeSeats.remove(seat); + + // Failure to acquire is not user-specific + userSpecificFailure = false; + + } + + } + + // Too many connections by this user + if (userSpecificFailure) + throw new GuacamoleClientTooManyException("Cannot connect. Connection group already in use by this user."); + + // Too many connections, but not necessarily due purely to this user + else + throw new GuacamoleResourceConflictException("Cannot connect. This connection is in use."); + + } + + @Override + protected void release(AuthenticatedUser user, ModeledConnection connection) { + activeSeats.remove(new Seat(user.getUser().getIdentifier(), connection.getIdentifier())); + activeConnections.remove(connection.getIdentifier()); + } + + @Override + protected void acquire(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) throws GuacamoleException { + + // Get username + String username = user.getUser().getIdentifier(); + + // Attempt to aquire connection group according to per-user limits + Seat seat = new Seat(username, connectionGroup.getIdentifier()); + if (tryAdd(activeGroupSeats, seat, + connectionGroupDefaultMaxConnectionsPerUser)) { + + // Attempt to aquire connection group according to overall limits + if (tryAdd(activeGroups, connectionGroup.getIdentifier(), + connectionGroupDefaultMaxConnections)) + return; + + // Acquire failed + activeGroupSeats.remove(seat); + + // Failure to acquire is not user-specific + throw new GuacamoleResourceConflictException("Cannot connect. This connection group is in use."); + + } + + // Already in use by this user + throw new GuacamoleClientTooManyException("Cannot connect. Connection group already in use by this user."); + + } + + @Override + protected void release(AuthenticatedUser user, + ModeledConnectionGroup connectionGroup) { + activeGroupSeats.remove(new Seat(user.getUser().getIdentifier(), connectionGroup.getIdentifier())); + activeGroups.remove(connectionGroup.getIdentifier()); + } + +}