blob: a5d509b9d59217f15d00460a96c4637519d8b072 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.auth.jdbc.tunnel;
import com.google.common.collect.ConcurrentHashMultiset;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import org.apache.guacamole.GuacamoleClientTooManyException;
import org.apache.guacamole.auth.jdbc.connection.ModeledConnection;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleResourceConflictException;
import org.apache.guacamole.auth.jdbc.JDBCEnvironment;
import org.apache.guacamole.auth.jdbc.connectiongroup.ModeledConnectionGroup;
import org.apache.guacamole.auth.jdbc.user.RemoteAuthenticatedUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* GuacamoleTunnelService implementation which restricts concurrency for each
* connection and group according to a maximum number of connections and
* maximum number of connections per user.
*/
@Singleton
public class RestrictedGuacamoleTunnelService
extends AbstractGuacamoleTunnelService {
/**
* Logger for this class.
*/
private static final Logger logger = LoggerFactory.getLogger(RestrictedGuacamoleTunnelService.class);
/**
* The environment of the Guacamole server.
*/
@Inject
private JDBCEnvironment environment;
/**
* Set of all currently-active user/connection pairs (seats).
*/
private final ConcurrentHashMultiset<Seat> activeSeats = ConcurrentHashMultiset.<Seat>create();
/**
* Set of all currently-active connections.
*/
private final ConcurrentHashMultiset<String> activeConnections = ConcurrentHashMultiset.<String>create();
/**
* Set of all currently-active user/connection group pairs (seats).
*/
private final ConcurrentHashMultiset<Seat> activeGroupSeats = ConcurrentHashMultiset.<Seat>create();
/**
* Set of all currently-active connection groups.
*/
private final ConcurrentHashMultiset<String> activeGroups = ConcurrentHashMultiset.<String>create();
/**
* The total number of active connections within this instance of
* Guacamole.
*/
private final AtomicInteger totalActiveConnections = new AtomicInteger(0);
/**
* 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 <T>
* 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 <T> boolean tryAdd(ConcurrentHashMultiset<T> 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
}
}
/**
* Attempts to increment the given AtomicInteger without exceeding the
* specified maximum value. If the AtomicInteger cannot be incremented
* without exceeding the maximum, false is returned.
*
* @param counter
* The AtomicInteger to attempt to increment.
*
* @param max
* The maximum value that the given AtomicInteger should contain, or
* zero if no limit applies.
*
* @return
* true if the AtomicInteger was successfully incremented without
* exceeding the specified maximum, false if the AtomicInteger could
* not be incremented.
*/
private boolean tryIncrement(AtomicInteger counter, int max) {
// Repeatedly attempt to increment the given AtomicInteger until we
// explicitly succeed or explicitly fail
while (true) {
// Get current value
int count = counter.get();
// Bail out if the maximum has already been reached
if (count >= max && max != 0)
return false;
// Attempt to increment
if (counter.compareAndSet(count, count+1))
return true;
// Try again if unsuccessful
}
}
@Override
protected ModeledConnection acquire(RemoteAuthenticatedUser user,
List<ModeledConnection> connections, boolean includeFailoverOnly)
throws GuacamoleException {
// Do not acquire connection unless within overall limits
if (!tryIncrement(totalActiveConnections, environment.getAbsoluteMaxConnections()))
throw new GuacamoleResourceConflictException("Cannot connect. Overall maximum connections reached.");
// Get username
String username = user.getIdentifier();
// Sort connections in ascending order of usage
ModeledConnection[] sortedConnections = connections.toArray(new ModeledConnection[connections.size()]);
Arrays.sort(sortedConnections, new Comparator<ModeledConnection>() {
@Override
public int compare(ModeledConnection a, ModeledConnection b) {
// Active connections
int connA = getActiveConnections(a).size();
int connB = getActiveConnections(b).size();
// Assigned weight
int weightA = a.getConnectionWeight();
int weightB = b.getConnectionWeight();
// Calculated weight of connections
int calcWeightA = connA * weightB;
int calcWeightB = connB * weightA;
// If calculated weights are equal, return difference in assigned weight
if (calcWeightA == calcWeightB)
return (weightA - weightB);
// Return different in calculated weights
return (calcWeightA - calcWeightB);
}
});
// Track whether acquire fails due to user-specific limits
boolean userSpecificFailure = true;
// Return the first unreserved connection
for (ModeledConnection connection : sortedConnections) {
// If connection weight is less than 1 this host is disabled and should not be used.
if (connection.getConnectionWeight() < 1) {
logger.debug("Weight for {} is < 1, connection will be skipped.", connection.getName());
continue;
}
// Skip connections which are failover-only if they are excluded
// from this connection attempt
if (!includeFailoverOnly && connection.isFailoverOnly())
continue;
// Attempt to aquire connection according to per-user limits
Seat seat = new Seat(username, connection.getIdentifier());
if (tryAdd(activeSeats, seat,
connection.getMaxConnectionsPerUser())) {
// Attempt to aquire connection according to overall limits
if (tryAdd(activeConnections, connection.getIdentifier(),
connection.getMaxConnections()))
return connection;
// Acquire failed - retry with next connection
activeSeats.remove(seat);
// Failure to acquire is not user-specific
userSpecificFailure = false;
}
}
// Acquire failed
totalActiveConnections.decrementAndGet();
// Too many connections by this user
if (userSpecificFailure)
throw new GuacamoleClientTooManyException("Cannot connect. Connection 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(RemoteAuthenticatedUser user, ModeledConnection connection) {
activeSeats.remove(new Seat(user.getIdentifier(), connection.getIdentifier()));
activeConnections.remove(connection.getIdentifier());
totalActiveConnections.decrementAndGet();
}
@Override
protected void acquire(RemoteAuthenticatedUser user,
ModeledConnectionGroup connectionGroup) throws GuacamoleException {
// Get username
String username = user.getIdentifier();
// Attempt to aquire connection group according to per-user limits
Seat seat = new Seat(username, connectionGroup.getIdentifier());
if (tryAdd(activeGroupSeats, seat,
connectionGroup.getMaxConnectionsPerUser())) {
// Attempt to aquire connection group according to overall limits
if (tryAdd(activeGroups, connectionGroup.getIdentifier(),
connectionGroup.getMaxConnections()))
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(RemoteAuthenticatedUser user,
ModeledConnectionGroup connectionGroup) {
activeGroupSeats.remove(new Seat(user.getIdentifier(), connectionGroup.getIdentifier()));
activeGroups.remove(connectionGroup.getIdentifier());
}
}