diff --git a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java index 31e9c6389..a50389f48 100644 --- a/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/JDBCAuthenticationProviderModule.java @@ -46,7 +46,6 @@ import org.glyptodon.guacamole.auth.jdbc.security.SHA256PasswordEncryptionServic import org.glyptodon.guacamole.auth.jdbc.security.SaltService; import org.glyptodon.guacamole.auth.jdbc.security.SecureRandomSaltService; import org.glyptodon.guacamole.auth.jdbc.permission.SystemPermissionService; -import org.glyptodon.guacamole.auth.jdbc.socket.UnrestrictedGuacamoleSocketService; import org.glyptodon.guacamole.auth.jdbc.user.UserService; import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; import org.glyptodon.guacamole.auth.jdbc.permission.ConnectionGroupPermissionMapper; @@ -77,15 +76,27 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule { */ private final Environment environment; + /** + * The service class to use to provide GuacamoleSockets for each + * connection. + */ + private final Class socketServiceClass; + /** * Creates a new JDBC authentication provider module that configures the - * various injected base classes using the given environment. + * various injected base classes using the given environment, and provides + * connections using the given socket service. * * @param environment * The environment to use to configure injected classes. + * + * @param socketServiceClass + * The socket service to use to provide sockets for connections. */ - public JDBCAuthenticationProviderModule(Environment environment) { + public JDBCAuthenticationProviderModule(Environment environment, + Class socketServiceClass) { this.environment = environment; + this.socketServiceClass = socketServiceClass; } @Override @@ -135,8 +146,8 @@ public class JDBCAuthenticationProviderModule extends MyBatisModule { bind(UserPermissionService.class); bind(UserService.class); - // Bind appropriate socket service based on policy - bind(GuacamoleSocketService.class).to(UnrestrictedGuacamoleSocketService.class); + // Bind provided socket service + bind(GuacamoleSocketService.class).to(socketServiceClass); } 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 d684f29c9..084823684 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 @@ -29,6 +29,7 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; import org.glyptodon.guacamole.auth.jdbc.user.AuthenticatedUser; import org.glyptodon.guacamole.auth.jdbc.connection.ModeledConnection; import org.glyptodon.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup; @@ -138,6 +139,109 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS protected abstract void release(AuthenticatedUser user, ModeledConnection connection); + /** + * Returns a guacamole configuration containing the protocol and parameters + * from the given connection. If tokens are used in the connection + * parameter values, credentials from the given user will be substituted + * appropriately. + * + * @param user + * The user whose credentials should be used if necessary. + * + * @param connection + * The connection whose protocol and parameters should be added to the + * returned configuration. + * + * @return + * A GuacamoleConfiguration containing the protocol and parameters from + * the given connection. + */ + private GuacamoleConfiguration getGuacamoleConfiguration(AuthenticatedUser user, + ModeledConnection connection) { + + // Generate configuration from available data + GuacamoleConfiguration config = new GuacamoleConfiguration(); + + // Set protocol from connection + ConnectionModel model = connection.getModel(); + config.setProtocol(model.getProtocol()); + + // Set parameters from associated data + Collection parameters = parameterMapper.select(connection.getIdentifier()); + for (ParameterModel parameter : parameters) + config.setParameter(parameter.getName(), parameter.getValue()); + + // Build token filter containing credential tokens + TokenFilter tokenFilter = new TokenFilter(); + StandardTokens.addStandardTokens(tokenFilter, user.getCredentials()); + + // Filter the configuration + tokenFilter.filterValues(config.getParameters()); + + return config; + + } + + /** + * 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. + * + * @param record + * The record to save. + */ + private void saveConnectionRecord(String identifier, + ActiveConnectionRecord record) { + + // Get associated models + AuthenticatedUser user = record.getUser(); + UserModel userModel = user.getUser().getModel(); + ConnectionRecordModel recordModel = new ConnectionRecordModel(); + + // Copy user information and timestamps into new record + recordModel.setUserID(userModel.getObjectID()); + recordModel.setUsername(userModel.getIdentifier()); + recordModel.setConnectionIdentifier(identifier); + recordModel.setStartDate(record.getStartDate()); + recordModel.setEndDate(new Date()); + + // Insert connection record + connectionRecordMapper.insert(recordModel); + + } + + /** + * Returns an unconfigured GuacamoleSocket that is already connected to + * guacd as specified in guacamole.properties, using SSL if necessary. + * + * @return + * An unconfigured GuacamoleSocket, already connected to guacd. + * + * @throws GuacamoleException + * If an error occurs while connecting to guacd, or while parsing + * guacd-related properties. + */ + private GuacamoleSocket getUnconfiguredGuacamoleSocket() + throws GuacamoleException { + + // Use SSL if requested + if (environment.getProperty(Environment.GUACD_SSL, true)) + return new InetGuacamoleSocket( + environment.getRequiredProperty(Environment.GUACD_HOSTNAME), + environment.getRequiredProperty(Environment.GUACD_PORT) + ); + + // Otherwise, just use straight TCP + return new InetGuacamoleSocket( + environment.getRequiredProperty(Environment.GUACD_HOSTNAME), + environment.getRequiredProperty(Environment.GUACD_PORT) + ); + + } + /** * Creates a socket for the given user which connects to the given * connection, which MUST already be acquired via acquire(). The given @@ -173,28 +277,10 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS final ActiveConnectionRecord activeConnection = new ActiveConnectionRecord(user); // Get relevant identifiers + final AtomicBoolean released = new AtomicBoolean(false); final String identifier = connection.getIdentifier(); final String parentIdentifier = connection.getParentIdentifier(); - - // Generate configuration from available data - GuacamoleConfiguration config = new GuacamoleConfiguration(); - - // Set protocol from connection - ConnectionModel model = connection.getModel(); - config.setProtocol(model.getProtocol()); - - // Set parameters from associated data - Collection parameters = parameterMapper.select(identifier); - for (ParameterModel parameter : parameters) - config.setParameter(parameter.getName(), parameter.getValue()); - - // Build token filter containing credential tokens - TokenFilter tokenFilter = new TokenFilter(); - StandardTokens.addStandardTokens(tokenFilter, user.getCredentials()); - - // Filter the configuration - tokenFilter.filterValues(config.getParameters()); - + // Return new socket try { @@ -204,11 +290,9 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS // Return newly-reserved connection return new ConfiguredGuacamoleSocket( - new InetGuacamoleSocket( - environment.getRequiredProperty(Environment.GUACD_HOSTNAME), - environment.getRequiredProperty(Environment.GUACD_PORT) - ), - config + getUnconfiguredGuacamoleSocket(), + getGuacamoleConfiguration(user, connection), + info ) { @Override @@ -217,25 +301,20 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS // Attempt to close connection super.close(); - // Release connection upon close - activeConnections.remove(identifier, activeConnection); - activeConnectionGroups.remove(parentIdentifier, activeConnection); - release(user, connection); + // Release connection upon close, if not already released + if (released.compareAndSet(false, true)) { - UserModel userModel = user.getUser().getModel(); - ConnectionRecordModel recordModel = new ConnectionRecordModel(); + // Release connection + activeConnections.remove(identifier, activeConnection); + activeConnectionGroups.remove(parentIdentifier, activeConnection); + release(user, connection); - // Copy user information and timestamps into new record - recordModel.setUserID(userModel.getObjectID()); - recordModel.setUsername(userModel.getIdentifier()); - recordModel.setConnectionIdentifier(identifier); - recordModel.setStartDate(activeConnection.getStartDate()); - recordModel.setEndDate(new Date()); + // Save record to database + saveConnectionRecord(identifier, activeConnection); - // Insert connection record - connectionRecordMapper.insert(recordModel); + } - } + } // end close() }; @@ -244,10 +323,12 @@ public abstract class AbstractGuacamoleSocketService implements GuacamoleSocketS // Release connection in case of error catch (GuacamoleException e) { - // Atomically release access to connection - activeConnections.remove(identifier, activeConnection); - activeConnectionGroups.remove(parentIdentifier, activeConnection); - release(user, connection); + // Release connection if not already released + if (released.compareAndSet(false, true)) { + activeConnections.remove(identifier, activeConnection); + activeConnectionGroups.remove(parentIdentifier, activeConnection); + release(user, connection); + } throw e; 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 f32669446..e6d520b87 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 @@ -60,6 +60,18 @@ public class ActiveConnectionRecord implements ConnectionRecord { this.user = user; } + /** + * Returns the user that connected to the connection associated with this + * connection record. + * + * @return + * The user that connected to the connection associated with this + * connection record. + */ + public AuthenticatedUser getUser() { + return user; + } + @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/MultiseatGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java new file mode 100644 index 000000000..a55e31780 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/MultiseatGuacamoleSocketService.java @@ -0,0 +1,138 @@ +/* + * 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; + + +/** + * 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. + * + * @author Michael Jumper + */ +@Singleton +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()); + + @Override + protected ModeledConnection acquire(AuthenticatedUser user, + List connections) throws GuacamoleException { + + String username = user.getUser().getIdentifier(); + + // Return the first unreserved connection + for (ModeledConnection connection : connections) { + if (activeSeats.add(new Seat(username, 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) { + activeSeats.remove(new Seat(user.getUser().getIdentifier(), connection.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 new file mode 100644 index 000000000..8cab0b814 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/ReservedGuacamoleSocketService.java @@ -0,0 +1,183 @@ +/* + * 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/SingleSeatGuacamoleSocketService.java b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java new file mode 100644 index 000000000..e3ff6a9a4 --- /dev/null +++ b/extensions/guacamole-auth-jdbc/modules/guacamole-auth-jdbc-base/src/main/java/org/glyptodon/guacamole/auth/jdbc/socket/SingleSeatGuacamoleSocketService.java @@ -0,0 +1,72 @@ +/* + * 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; + + +/** + * GuacamoleSocketService implementation which allows exactly one use + * of any connection at any time. Concurrent usage of any kind is not allowed. + * + * @author Michael Jumper + */ +@Singleton +public class SingleSeatGuacamoleSocketService + 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()); + } + +} 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 8547fcf3d..c33787a38 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 @@ -29,9 +29,15 @@ import org.glyptodon.guacamole.net.auth.AuthenticationProvider; import org.glyptodon.guacamole.net.auth.Credentials; 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.SingleSeatGuacamoleSocketService; +import org.glyptodon.guacamole.auth.jdbc.socket.UnrestrictedGuacamoleSocketService; import org.glyptodon.guacamole.auth.jdbc.user.UserContextService; import org.glyptodon.guacamole.environment.Environment; import org.glyptodon.guacamole.environment.LocalEnvironment; +import org.glyptodon.guacamole.properties.GuacamoleProperties; /** * Provides a MySQL based implementation of the AuthenticationProvider @@ -48,6 +54,55 @@ public class MySQLAuthenticationProvider implements AuthenticationProvider { */ private final Injector injector; + /** + * Returns the appropriate socket service class given the Guacamole + * environment. The class is chosen based on configuration options that + * dictate concurrent usage policy. + * + * @param environment + * The environment of the Guacamole server. + * + * @return + * The socket service class that matches the concurrent usage policy + * options set in the Guacamole environment. + * + * @throws GuacamoleException + * If an error occurs while reading the configuration options. + */ + private Class + getSocketServiceClass(Environment environment) + throws GuacamoleException { + + // Read concurrency-related properties + boolean disallowSimultaneous = environment.getProperty(MySQLGuacamoleProperties.MYSQL_DISALLOW_SIMULTANEOUS_CONNECTIONS, false); + boolean disallowDuplicate = environment.getProperty(MySQLGuacamoleProperties.MYSQL_DISALLOW_DUPLICATE_CONNECTIONS, true); + + if (disallowSimultaneous) { + + // Connections may not be used concurrently + if (disallowDuplicate) + return SingleSeatGuacamoleSocketService.class; + + // Connections are reserved for a single user when in use + else + return ReservedGuacamoleSocketService.class; + + } + + else { + + // Connections may be used concurrently, but only once per user + if (disallowDuplicate) + return MultiseatGuacamoleSocketService.class; + + // Connection use is not restricted + else + return UnrestrictedGuacamoleSocketService.class; + + } + + } + /** * Creates a new MySQLAuthenticationProvider that reads and writes * authentication data to a MySQL database defined by properties in @@ -69,7 +124,7 @@ public class MySQLAuthenticationProvider implements AuthenticationProvider { new MySQLAuthenticationProviderModule(environment), // Configure JDBC authentication core - new JDBCAuthenticationProviderModule(environment) + new JDBCAuthenticationProviderModule(environment, getSocketServiceClass(environment)) );