* Abstract servlet which provides an authenticatedService() function that
* is only called if the HTTP request is authenticated, or the current
* HTTP session has already been authenticated.
- *
+ *
* Authorized configurations are retrieved using the authentication provider
* defined in guacamole.properties. The authentication provider has access
* to the request and session, in addition to any submitted username and
* password, in order to authenticate the user.
- *
+ *
* All authorized configurations will be stored in the current HttpSession.
- *
+ *
* Success and failure are logged.
- *
+ *
* @author Michael Jumper
*/
public abstract class AuthenticatingHttpServlet extends HttpServlet {
private Logger logger = LoggerFactory.getLogger(AuthenticatingHttpServlet.class);
-
+
/**
* The session attribute holding the map of configurations.
*/
private static final String CONFIGURATIONS_ATTRIBUTE = "GUAC_CONFIGS";
-
+
/**
* The session attribute holding the credentials authorizing this session.
*/
private static final String CREDENTIALS_ATTRIBUTE = "GUAC_CREDS";
-
+
/**
* The AuthenticationProvider to use to authenticate all requests.
*/
/**
* Notifies all listeners in the given collection that authentication has
* failed.
- *
+ *
* @param listeners A collection of all listeners that should be notified.
* @param credentials The credentials associated with the authentication
* request that failed.
*/
private void notifyFailed(Collection listeners, Credentials credentials) {
-
+
// Build event for auth failure
AuthenticationFailureEvent event = new AuthenticationFailureEvent(credentials);
-
+
// Notify all listeners
for (Object listener : listeners) {
try {
logger.error("Error notifying AuthenticationFailureListener.", e);
}
}
-
+
}
/**
* Notifies all listeners in the given collection that authentication was
* successful.
- *
+ *
* @param listeners A collection of all listeners that should be notified.
* @param credentials The credentials associated with the authentication
* request that succeeded.
*/
private boolean notifySuccess(Collection listeners, Credentials credentials)
throws GuacamoleException {
-
+
// Build event for auth success
AuthenticationSuccessEvent event = new AuthenticationSuccessEvent(credentials);
-
+
// Notify all listeners
for (Object listener : listeners) {
if (listener instanceof AuthenticationSuccessListener) {
// Cancel immediately if hook returns false
if (!((AuthenticationSuccessListener) listener).authenticationSucceeded(event))
return false;
-
+
}
}
return true;
-
+
}
-
+
/**
* Sends a predefined, generic error message to the user, along with a
* "403 - Forbidden" HTTP status code in the response.
- *
+ *
* @param response The response to send the error within.
* @throws IOException If an error occurs while sending the error.
*/
private void failAuthentication(HttpServletResponse response) throws IOException {
response.sendError(HttpServletResponse.SC_FORBIDDEN);
}
-
+
/**
* Returns the credentials associated with the given session.
- *
+ *
* @param session The session to retrieve credentials from.
* @return The credentials associated with the given session.
*/
/**
* Returns the configurations associated with the given session.
- *
+ *
* @param session The session to retrieve configurations from.
* @return The configurations associated with the given session.
*/
protected Map<String, GuacamoleConfiguration> getConfigurations(HttpSession session) {
return (Map<String, GuacamoleConfiguration>) session.getAttribute(CONFIGURATIONS_ATTRIBUTE);
}
-
+
@Override
protected void service(HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
failAuthentication(response);
return;
}
-
+
// Retrieve username and password from parms
String username = request.getParameter("username");
String password = request.getParameter("password");
/******** HANDLE FAILED AUTHENTICATION ********/
-
+
// If error retrieving configs, fail authentication, notify listeners
catch (GuacamoleException e) {
logger.error("Error retrieving configuration(s) for user \"{}\".",
failAuthentication(response);
return;
}
-
+
// If no configs, fail authentication, notify listeners
if (configs == null) {
logger.warn("Authentication attempt from {} for user \"{}\" failed.",
request.getRemoteAddr(), credentials.getUsername());
-
+
notifyFailed(listeners, credentials);
failAuthentication(response);
return;
/******** HANDLE SUCCESSFUL AUTHENTICATION ********/
-
+
try {
// Otherwise, authentication has been succesful
failAuthentication(response);
return;
}
-
+
}
catch (GuacamoleException e) {
-
+
// Cancel authentication success if hook throws exception
logger.error("Successful authentication canceled by error in hook.", e);
failAuthentication(response);
return;
-
+
}
// Associate configs and credentials with session
* Authenticates users against a static list of username/password pairs.
* Each username/password may be associated with multiple configurations.
* This list is stored in an XML file which is reread if modified.
- *
+ *
* @author Michael Jumper, Michal Kotas
*/
public class BasicFileAuthenticationProvider implements AuthenticationProvider {
private Logger logger = LoggerFactory.getLogger(BasicFileAuthenticationProvider.class);
-
+
private long mappingTime;
private Map<String, AuthInfo> mapping;
throw new GuacamoleException("Missing \"basic-user-mapping\" parameter required for basic login.");
logger.info("Reading user mapping file: {}", mapFile);
-
+
// Parse document
try {
// If no mapping available, report as such
if (mapping == null)
throw new GuacamoleException("User mapping could not be read.");
-
+
// Validate and return info for given user and pass
AuthInfo info = mapping.get(credentials.getUsername());
if (info != null && info.validate(credentials.getUsername(), credentials.getPassword()))
}
break;
-
+
case CONNECTION:
if (localName.equals("connection")) {
return;
}
- break;
+ break;
case PROTOCOL:
currentConnection = attributes.getValue("name");
if (currentConnection == null)
throw new SAXException("Attribute \"name\" required for connection tag.");
-
+
// Next state
state = State.CONNECTION;
return;
// Associate protocol with default connection
currentConnection = "DEFAULT";
-
+
// Next state
state = State.DEFAULT_CONNECTION_PROTOCOL;
return;
// Associate parameter with default connection
currentConnection = "DEFAULT";
-
+
currentParameter = attributes.getValue("name");
if (currentParameter == null)
throw new SAXException("Attribute \"name\" required for param tag.");
}
break;
-
+
case CONNECTION:
if (localName.equals("protocol")) {
return;
}
- break;
+ break;
}
public void characters(char[] ch, int start, int length) throws SAXException {
String str = new String(ch, start, length);
-
+
switch (state) {
case PROTOCOL:
current.getConfiguration(currentConnection)
.setParameter(currentParameter, str);
return;
-
+
}
if (str.trim().length() != 0)
/**
* Connects users to a tunnel associated with the authorized configuration
* having the given ID.
- *
+ *
* @author Michael Jumper
*/
public class BasicGuacamoleTunnelServlet extends AuthenticatingHttpServlet {
Map<String, GuacamoleConfiguration> configs,
HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
-
+
// If authenticated, respond as tunnel
tunnelServlet.service(request, response);
-
+
}
/**
* Notifies all listeners in the given collection that a tunnel has been
* connected.
- *
+ *
* @param listeners A collection of all listeners that should be notified.
* @param credentials The credentials associated with the authentication
* request that connected the tunnel.
private boolean notifyConnect(Collection listeners,
Credentials credentials, GuacamoleTunnel tunnel)
throws GuacamoleException {
-
+
// Build event for auth success
TunnelConnectEvent event = new TunnelConnectEvent(credentials, tunnel);
-
+
// Notify all listeners
for (Object listener : listeners) {
if (listener instanceof TunnelConnectListener) {
// Cancel immediately if hook returns false
if (!((TunnelConnectListener) listener).tunnelConnected(event))
return false;
-
+
}
}
return true;
-
+
}
/**
* Notifies all listeners in the given collection that a tunnel has been
* closed.
- *
+ *
* @param listeners A collection of all listeners that should be notified.
* @param credentials The credentials associated with the authentication
* request that closed the tunnel.
private boolean notifyClose(Collection listeners,
Credentials credentials, GuacamoleTunnel tunnel)
throws GuacamoleException {
-
+
// Build event for auth success
TunnelCloseEvent event = new TunnelCloseEvent(credentials, tunnel);
-
+
// Notify all listeners
for (Object listener : listeners) {
if (listener instanceof TunnelCloseListener) {
// Cancel immediately if hook returns false
if (!((TunnelCloseListener) listener).tunnelClosed(event))
return false;
-
+
}
}
return true;
-
+
}
/**
protected GuacamoleTunnel doConnect(HttpServletRequest request) throws GuacamoleException {
HttpSession httpSession = request.getSession(true);
-
+
// Get listeners
final SessionListenerCollection listeners;
try {
// Get ID of connection
String id = request.getParameter("id");
-
+
// Get credentials
final Credentials credentials = getCredentials(httpSession);
-
+
// Get authorized configs
Map<String, GuacamoleConfiguration> configs = getConfigurations(httpSession);
logger.warn("Configuration id={} not found.", id);
throw new GuacamoleSecurityException("Requested configuration is not authorized.");
}
-
+
logger.info("Successful connection from {} to \"{}\".", request.getRemoteAddr(), id);
// Configure and connect socket
// Only close if not canceled
if (!notifyClose(listeners, credentials, this))
throw new GuacamoleException("Tunnel close canceled by listener.");
-
+
// Close if no exception due to listener
super.close();
-
+
}
-
+
};
// Notify listeners about connection
logger.info("Connection canceled by listener.");
return null;
}
-
+
return tunnel;
}
/**
* Simple dummy AuthenticatingHttpServlet which provides an endpoint for arbitrary
* authentication requests that do not expect a response.
- *
+ *
* @author Michael Jumper
*/
public class BasicLogin extends AuthenticatingHttpServlet {
private Logger logger = LoggerFactory.getLogger(BasicLogin.class);
-
+
@Override
protected void authenticatedService(
Map<String, GuacamoleConfiguration> configs,
/**
* Logs out the current user by invalidating the associated HttpSession and
* redirecting the user to the login page.
- *
+ *
* @author Michael Jumper
*/
public class BasicLogout extends HttpServlet {
/**
* Simple HttpServlet which outputs XML containing a list of all authorized
* configurations for the current user.
- *
+ *
* @author Michael Jumper
*/
public class ConfigurationList extends AuthenticatingHttpServlet {
// Do not cache
response.setHeader("Cache-Control", "no-cache");
-
+
// Write XML
response.setHeader("Content-Type", "text/xml");
PrintWriter out = response.getWriter();
out.println("<configs>");
-
+
for (Entry<String, GuacamoleConfiguration> entry : configs.entrySet()) {
GuacamoleConfiguration config = entry.getValue();
/**
* A ClassLoader implementation which finds classes within a configurable
* directory. This directory is set within guacamole.properties.
- *
+ *
* @author Michael Jumper
*/
public class GuacamoleClassLoader extends ClassLoader {
-
+
private URLClassLoader classLoader = null;
private static GuacamoleException exception = null;
private static GuacamoleClassLoader instance = null;
-
+
static {
-
+
try {
// Attempt to create singleton classloader which loads classes from
// all .jar's in the lib directory defined in guacamole.properties
});
}
-
+
catch (PrivilegedActionException e) {
// On error, record exception
exception = (GuacamoleException) e.getException();
}
-
+
}
private GuacamoleClassLoader(File libDirectory) throws GuacamoleException {
// If no directory provided, just direct requests to parent classloader
if (libDirectory == null)
return;
-
+
// Validate directory is indeed a directory
if (!libDirectory.isDirectory())
throw new GuacamoleException(libDirectory + " is not a directory.");
-
+
// Get list of URLs for all .jar's in the lib directory
Collection<URL> jarURLs = new ArrayList<URL>();
for (File file : libDirectory.listFiles(new FilenameFilter() {
@Override
public boolean accept(File dir, String name) {
-
+
// If it ends with .jar, accept the file
return name.endsWith(".jar");
-
+
}
})) {
try {
-
+
// Add URL for the .jar to the jar URL list
jarURLs.add(file.toURI().toURL());
-
+
}
catch (MalformedURLException e) {
throw new GuacamoleException(e);
}
-
+
}
-
+
// Set delegate classloader to new URLClassLoader which loads from the
// .jars found above.
jarURLs.toArray(urls),
getClass().getClassLoader()
);
-
+
}
/**
* Returns an instance of a GuacamoleClassLoader which finds classes
* within the directory configured in guacamole.properties.
- *
+ *
* @return An instance of a GuacamoleClassLoader.
* @throws GuacamoleException If no instance could be returned due to an
* error.
*/
public static GuacamoleClassLoader getInstance() throws GuacamoleException {
-
+
// If instance could not be created, rethrow original exception
if (exception != null) throw exception;
-
+
return instance;
}
// If no classloader, use default loader
if (classLoader == null)
return Class.forName(name);
-
+
// Otherwise, delegate
return classLoader.loadClass(name);
* Simple ServletContextListener which loads a WebSocket tunnel implementation
* if available, using the Servlet 3.0 API to dynamically load and install
* the tunnel servlet.
- *
+ *
* Note that because Guacamole depends on the Servlet 2.5 API, and 3.0 may
* not be available or needed if WebSocket is not desired, the 3.0 API is
* detected and invoked dynamically via reflection.
- *
+ *
* @author Michael Jumper
*/
public class WebSocketSupportLoader implements ServletContextListener {
// Attempt to find WebSocket servlet
Class<Servlet> servlet = (Class<Servlet>) GuacamoleClassLoader.getInstance().findClass(
"net.sourceforge.guacamole.net.basic.BasicGuacamoleWebSocketTunnelServlet"
- );
+ );
// Dynamically add servlet IF SERVLET 3.0 API AVAILABLE!
try {
* collection is stored within the HttpSession, and will be reused if available.
* Each listener is instantiated once per session. Listeners are singleton
* classes within the session, but not globally.
- *
+ *
* @author Michael Jumper
*/
public class SessionListenerCollection extends AbstractCollection {
* session.
*/
private Collection listeners;
-
+
/**
* Creates a new SessionListenerCollection which stores all listeners
* defined in guacamole.properties in the provided session. If listeners
* are already stored in the provided session, those listeners are used
* instead.
- *
+ *
* @param session The HttpSession to store listeners within.
* @throws GuacamoleException If an error occurs while instantiating new
* listeners.
*/
public SessionListenerCollection(HttpSession session) throws GuacamoleException {
-
+
// Pull cached listeners from session
listeners = (Collection) session.getAttribute(SESSION_ATTRIBUTE);
// Store listeners for next time
session.setAttribute(SESSION_ATTRIBUTE, listeners);
-
+
}
-
+
}
-
+
@Override
public Iterator iterator() {
return listeners.iterator();
public int size() {
return listeners.size();
}
-
+
}
/**
* A GuacamoleProperty whose value is the name of a class to use to
* authenticate users. This class must implement AuthenticationProvider.
- *
+ *
* @author Michael Jumper
*/
public abstract class AuthenticationProviderProperty implements GuacamoleProperty<AuthenticationProvider> {
/**
* Properties used by the default Guacamole web application.
- *
+ *
* @author Michael Jumper
*/
public class BasicGuacamoleProperties {
/**
* A GuacamoleProperty whose value is a comma-separated list of class names,
* where each class will be used as a listener for events.
- *
+ *
* @author Michael Jumper
*/
public abstract class EventListenersProperty implements GuacamoleProperty<Collection<Class>> {
// Parse list
String[] classNames = classNameList.split(",[\\s]*");
-
+
// Fill list of classes
Collection<Class> listeners = new ArrayList<Class>();
try {