GUACAMOLE-364: Merge changes restoring extension support for event listeners.

This commit is contained in:
Michael Jumper
2017-10-06 09:48:33 -07:00
18 changed files with 887 additions and 101 deletions

View File

@@ -26,6 +26,11 @@ import org.apache.guacamole.net.auth.UserContext;
* An event which is triggered whenever a user's credentials pass * An event which is triggered whenever a user's credentials pass
* authentication. The credentials that passed authentication are included * authentication. The credentials that passed authentication are included
* within this event, and can be retrieved using getCredentials(). * within this event, and can be retrieved using getCredentials().
* <p>
* If a {@link org.apache.guacamole.net.event.listener.Listener} throws
* a GuacamoleException when handling an event of this type, successful authentication
* is effectively <em>vetoed</em> and will be subsequently processed as though the
* authentication failed.
*/ */
public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent { public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent {

View File

@@ -28,6 +28,10 @@ import org.apache.guacamole.net.auth.UserContext;
* being closed can be accessed through getTunnel(), and the UserContext * being closed can be accessed through getTunnel(), and the UserContext
* associated with the request which is closing the tunnel can be retrieved * associated with the request which is closing the tunnel can be retrieved
* with getUserContext(). * with getUserContext().
* <p>
* If a {@link org.apache.guacamole.net.event.listener.Listener} throws
* a GuacamoleException when handling an event of this type, the request to close
* the tunnel is effectively <em>vetoed</em> and will remain connected.
*/ */
public class TunnelCloseEvent implements UserEvent, CredentialEvent, TunnelEvent { public class TunnelCloseEvent implements UserEvent, CredentialEvent, TunnelEvent {

View File

@@ -28,6 +28,10 @@ import org.apache.guacamole.net.auth.UserContext;
* being connected can be accessed through getTunnel(), and the UserContext * being connected can be accessed through getTunnel(), and the UserContext
* associated with the request which is connecting the tunnel can be retrieved * associated with the request which is connecting the tunnel can be retrieved
* with getUserContext(). * with getUserContext().
* <p>
* If a {@link org.apache.guacamole.net.event.listener.Listener} throws
* a GuacamoleException when handling an event of this type, the tunnel connection
* is effectively <em>vetoed</em> and will be subsequently closed.
*/ */
public class TunnelConnectEvent implements UserEvent, CredentialEvent, TunnelEvent { public class TunnelConnectEvent implements UserEvent, CredentialEvent, TunnelEvent {

View File

@@ -26,19 +26,26 @@ import org.apache.guacamole.net.event.AuthenticationFailureEvent;
* A listener whose authenticationFailed() hook will fire immediately * A listener whose authenticationFailed() hook will fire immediately
* after a user's authentication attempt fails. Note that this hook cannot * after a user's authentication attempt fails. Note that this hook cannot
* be used to cancel the authentication failure. * be used to cancel the authentication failure.
*
* @deprecated
* Listeners should instead implement the {@link Listener} interface.
*/ */
public interface AuthenticationFailureListener { @Deprecated
public interface AuthenticationFailureListener {
/** /**
* Event hook which fires immediately after a user's authentication attempt * Event hook which fires immediately after a user's authentication attempt
* fails. * fails.
* *
* @param e The AuthenticationFailureEvent describing the authentication * @param e
* failure that just occurred. * The AuthenticationFailureEvent describing the authentication
* @throws GuacamoleException If an error occurs while handling the * failure that just occurred.
* authentication failure event. Note that *
* throwing an exception will NOT cause the * @throws GuacamoleException
* authentication failure to be canceled. * If an error occurs while handling the authentication failure event.
* Note that throwing an exception will NOT cause the authentication
* failure to be canceled (which makes no sense), but it will prevent
* subsequent listeners from receiving the notification.
*/ */
void authenticationFailed(AuthenticationFailureEvent e) void authenticationFailed(AuthenticationFailureEvent e)
throws GuacamoleException; throws GuacamoleException;

View File

@@ -27,7 +27,11 @@ import org.apache.guacamole.net.event.AuthenticationSuccessEvent;
* authentication attempt succeeds. If a user successfully authenticates, * authentication attempt succeeds. If a user successfully authenticates,
* the authenticationSucceeded() hook has the opportunity to cancel the * the authenticationSucceeded() hook has the opportunity to cancel the
* authentication and force it to fail. * authentication and force it to fail.
*
* @deprecated
* Listeners should instead implement the {@link Listener} interface.
*/ */
@Deprecated
public interface AuthenticationSuccessListener { public interface AuthenticationSuccessListener {
/** /**
@@ -35,15 +39,18 @@ public interface AuthenticationSuccessListener {
* succeeds. The return value of this hook dictates whether the * succeeds. The return value of this hook dictates whether the
* successful authentication attempt is canceled. * successful authentication attempt is canceled.
* *
* @param e The AuthenticationFailureEvent describing the authentication * @param e
* failure that just occurred. * The AuthenticationFailureEvent describing the authentication
* @return true if the successful authentication attempt should be * failure that just occurred.
* allowed, or false if the attempt should be denied, causing *
* the attempt to effectively fail. * @return
* @throws GuacamoleException If an error occurs while handling the * true if the successful authentication attempt should be
* authentication success event. Throwing an * allowed, or false if the attempt should be denied, causing
* exception will also cancel the authentication * the attempt to effectively fail.
* success. *
* @throws GuacamoleException
* If an error occurs while handling the authentication success event.
* Throwing an exception will also cancel the authentication success.
*/ */
boolean authenticationSucceeded(AuthenticationSuccessEvent e) boolean authenticationSucceeded(AuthenticationSuccessEvent e)
throws GuacamoleException; throws GuacamoleException;

View File

@@ -0,0 +1,51 @@
/*
* 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.net.event.listener;
import org.apache.guacamole.GuacamoleException;
/**
* A listener for events that occur in handing various Guacamole requests
* such as authentication, tunnel connect/close, etc. Listeners are registered
* through the extension manifest mechanism. When an event occurs, listeners
* are notified in the order in which they are declared in the manifest and
* continues until either all listeners have been notified or with the first
* listener that throws a GuacamoleException or other runtime exception.
*/
public interface Listener {
/**
* Notifies the recipient that an event has occurred.
* <p>
* Throwing an exception from an event listener can act to veto an action in
* progress for some event types. See the Javadoc for specific event types for
* details.
*
* @param event
* An object that describes the event that has occurred.
*
* @throws GuacamoleException
* If the listener wishes to stop notification of the event to subsequent
* listeners. For some event types, this acts to veto an action in progress;
* e.g. treating a successful authentication as though it failed.
*/
void handleEvent(Object event) throws GuacamoleException;
}

View File

@@ -23,26 +23,33 @@ import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.event.TunnelCloseEvent; import org.apache.guacamole.net.event.TunnelCloseEvent;
/** /**
* A listener whose tunnelClosed() hook will fire immediately after an * A listener whose tunnelClosed() hook will fire immediately before an
* existing tunnel is closed. * existing tunnel is closed.
*
* @deprecated
* Listeners should instead implement the {@link Listener} interface.
*/ */
@Deprecated
public interface TunnelCloseListener { public interface TunnelCloseListener {
/** /**
* Event hook which fires immediately after an existing tunnel is closed. * Event hook which fires immediately before an existing tunnel is closed.
* The return value of this hook dictates whether the tunnel is allowed to * The return value of this hook dictates whether the tunnel is allowed to
* be closed. * be closed.
* *
* @param e The TunnelCloseEvent describing the tunnel being closed and * @param e
* any associated credentials. * The TunnelCloseEvent describing the tunnel being closed and
* @return true if the tunnel should be allowed to be closed, or false * any associated credentials.
* if the attempt should be denied, causing the attempt to *
* effectively fail. * @return
* @throws GuacamoleException If an error occurs while handling the * true if the tunnel should be allowed to be closed, or false
* tunnel close event. Throwing an exception * if the attempt should be denied, causing the attempt to
* will also stop the tunnel from being closed. * effectively fail.
*
* @throws GuacamoleException
* If an error occurs while handling the tunnel close event. Throwing
* an exception will also stop the tunnel from being closed.
*/ */
boolean tunnelClosed(TunnelCloseEvent e) boolean tunnelClosed(TunnelCloseEvent e) throws GuacamoleException;
throws GuacamoleException;
} }

View File

@@ -25,25 +25,32 @@ import org.apache.guacamole.net.event.TunnelConnectEvent;
/** /**
* A listener whose tunnelConnected() hook will fire immediately after a new * A listener whose tunnelConnected() hook will fire immediately after a new
* tunnel is connected. * tunnel is connected.
*
* @deprecated
* Listeners should instead implement the {@link Listener} interface.
*/ */
@Deprecated
public interface TunnelConnectListener { public interface TunnelConnectListener {
/** /**
* Event hook which fires immediately after a new tunnel is connected. * Event hook which fires immediately after a new tunnel is connected.
* The return value of this hook dictates whether the tunnel is made visible * The return value of this hook dictates whether the tunnel is made visible
* to the session. * to the session.
* *
* @param e The TunnelConnectEvent describing the tunnel being connected and * @param e
* any associated credentials. * The TunnelConnectEvent describing the tunnel being connected and
* @return true if the tunnel should be allowed to be connected, or false * any associated credentials.
* if the attempt should be denied, causing the attempt to *
* effectively fail. * @return
* @throws GuacamoleException If an error occurs while handling the * true if the tunnel should be allowed to be connected, or false
* tunnel connect event. Throwing an exception * if the attempt should be denied, causing the attempt to
* will also stop the tunnel from being made * effectively fail.
* visible to the session. *
*/ * @throws GuacamoleException
boolean tunnelConnected(TunnelConnectEvent e) * If an error occurs while handling the tunnel connect event. Throwing
throws GuacamoleException; * an exception will also stop the tunnel from being made visible to the
* session.
*/
boolean tunnelConnected(TunnelConnectEvent e) throws GuacamoleException;
} }

View File

@@ -19,7 +19,6 @@
package org.apache.guacamole.extension; package org.apache.guacamole.extension;
import java.lang.reflect.InvocationTargetException;
import java.util.UUID; import java.util.UUID;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticatedUser;
@@ -66,58 +65,8 @@ public class AuthenticationProviderFacade implements AuthenticationProvider {
* The AuthenticationProvider subclass to instantiate. * The AuthenticationProvider subclass to instantiate.
*/ */
public AuthenticationProviderFacade(Class<? extends AuthenticationProvider> authProviderClass) { public AuthenticationProviderFacade(Class<? extends AuthenticationProvider> authProviderClass) {
authProvider = ProviderFactory.newInstance("authentication provider",
AuthenticationProvider instance = null; authProviderClass);
try {
// Attempt to instantiate the authentication provider
instance = authProviderClass.getConstructor().newInstance();
}
catch (NoSuchMethodException e) {
logger.error("The authentication extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.");
logger.debug("AuthenticationProvider is missing a default constructor.", e);
}
catch (SecurityException e) {
logger.error("The Java security mananager is preventing authentication extensions "
+ "from being loaded. Please check the configuration of Java or your "
+ "servlet container.");
logger.debug("Creation of AuthenticationProvider disallowed by security manager.", e);
}
catch (InstantiationException e) {
logger.error("The authentication extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.");
logger.debug("AuthenticationProvider cannot be instantiated.", e);
}
catch (IllegalAccessException e) {
logger.error("The authentication extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.");
logger.debug("Default constructor of AuthenticationProvider is not public.", e);
}
catch (IllegalArgumentException e) {
logger.error("The authentication extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.");
logger.debug("Default constructor of AuthenticationProvider cannot accept zero arguments.", e);
}
catch (InvocationTargetException e) {
// Obtain causing error - create relatively-informative stub error if cause is unknown
Throwable cause = e.getCause();
if (cause == null)
cause = new GuacamoleException("Error encountered during initialization.");
logger.error("Authentication extension failed to start: {}", cause.getMessage());
logger.debug("AuthenticationProvider instantiation failed.", e);
}
// Associate instance, if any
authProvider = instance;
} }
@Override @Override

View File

@@ -35,6 +35,7 @@ import java.util.Map;
import java.util.zip.ZipEntry; import java.util.zip.ZipEntry;
import java.util.zip.ZipException; import java.util.zip.ZipException;
import java.util.zip.ZipFile; import java.util.zip.ZipFile;
import org.apache.guacamole.net.event.listener.Listener;
import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.ObjectMapper; import org.codehaus.jackson.map.ObjectMapper;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
@@ -109,6 +110,11 @@ public class Extension {
*/ */
private final Collection<Class<AuthenticationProvider>> authenticationProviderClasses; private final Collection<Class<AuthenticationProvider>> authenticationProviderClasses;
/**
* The collection of all Listener classes defined within the extension.
*/
private final Collection<Class<?>> listenerClasses;
/** /**
* The resource for the small favicon for the extension. If provided, this * The resource for the small favicon for the extension. If provided, this
* will replace the default Guacamole icon. * will replace the default Guacamole icon.
@@ -265,6 +271,80 @@ public class Extension {
} }
/**
* Retrieve the Listener subclass having the given name. If
* the class having the given name does not exist or isn't actually a
* subclass of Listener, an exception will be thrown.
*
* @param name
* The name of the Listener class to retrieve.
*
* @return
* The subclass of Listener having the given name.
*
* @throws GuacamoleException
* If no such class exists, or if the class with the given name is not
* a subclass of Listener.
*/
@SuppressWarnings("unchecked") // We check this ourselves with isAssignableFrom()
private Class<Listener> getListenerClass(String name)
throws GuacamoleException {
try {
// Get listener class
Class<?> listenerClass = classLoader.loadClass(name);
// Verify the located class is actually a subclass of Listener
if (!Listener.class.isAssignableFrom(listenerClass))
throw new GuacamoleServerException("Listeners MUST implement a Listener subclass.");
// Return located class
return (Class<Listener>) listenerClass;
}
catch (ClassNotFoundException e) {
throw new GuacamoleException("Listener class not found.", e);
}
catch (LinkageError e) {
throw new GuacamoleException("Listener class cannot be loaded (wrong version of API?).", e);
}
}
/**
* Returns a new collection of all Listener subclasses having the given names.
* If any class does not exist or isn't actually subclass of Listener, an
* exception will be thrown, an no further Listener classes will be loaded.
*
* @param names
* The names of the AuthenticationProvider classes to retrieve.
*
* @return
* A new collection of all AuthenticationProvider subclasses having the
* given names.
*
* @throws GuacamoleException
* If any given class does not exist, or if any given class is not a
* subclass of AuthenticationProvider.
*/
private Collection<Class<?>> getListenerClasses(Collection<String> names)
throws GuacamoleException {
// If no classnames are provided, just return an empty list
if (names == null)
return Collections.<Class<?>>emptyList();
// Define all auth provider classes
Collection<Class<?>> classes = new ArrayList<Class<?>>(names.size());
for (String name : names)
classes.add(getListenerClass(name));
// Callers should not rely on modifying the result
return Collections.unmodifiableCollection(classes);
}
/** /**
* Loads the given file as an extension, which must be a .jar containing * Loads the given file as an extension, which must be a .jar containing
* a guac-manifest.json file describing its contents. * a guac-manifest.json file describing its contents.
@@ -363,6 +443,9 @@ public class Extension {
// Define authentication providers // Define authentication providers
authenticationProviderClasses = getAuthenticationProviderClasses(manifest.getAuthProviders()); authenticationProviderClasses = getAuthenticationProviderClasses(manifest.getAuthProviders());
// Define listeners
listenerClasses = getListenerClasses(manifest.getListeners());
// Get small icon resource if provided // Get small icon resource if provided
if (manifest.getSmallIcon() != null) if (manifest.getSmallIcon() != null)
smallIcon = new ClassPathResource(classLoader, "image/png", manifest.getSmallIcon()); smallIcon = new ClassPathResource(classLoader, "image/png", manifest.getSmallIcon());
@@ -488,6 +571,17 @@ public class Extension {
return authenticationProviderClasses; return authenticationProviderClasses;
} }
/**
* Returns all declared listener classes associated wit this extension. Listeners are
* declared within the extension manifest.
*
* @return
* All declared listener classes with this extension.
*/
public Collection<Class<?>> getListenerClasses() {
return listenerClasses;
}
/** /**
* Returns the resource for the small favicon for the extension. If * Returns the resource for the small favicon for the extension. If
* provided, this will replace the default Guacamole icon. * provided, this will replace the default Guacamole icon.

View File

@@ -87,6 +87,11 @@ public class ExtensionManifest {
*/ */
private Collection<String> authProviders; private Collection<String> authProviders;
/**
* The names of all listener classes within this extension, if any.
*/
private Collection<String> listeners;
/** /**
* The path to the small favicon. If provided, this will replace the default * The path to the small favicon. If provided, this will replace the default
* Guacamole icon. * Guacamole icon.
@@ -355,6 +360,32 @@ public class ExtensionManifest {
this.authProviders = authProviders; this.authProviders = authProviders;
} }
/**
* Returns the classnames of all listener classes within the extension.
* These classnames are defined within the manifest by the "listeners"
* property as an array of strings, where each string is a listener
* class name.
*
* @return
* A collection of classnames for all listeners within the extension.
*/
public Collection<String> getListeners() {
return listeners;
}
/**
* Sets the classnames of all listener classes within the extension.
* These classnames are defined within the manifest by the "listeners"
* property as an array of strings, where each string is a listener
* class name.
*
* @param listeners
* A collection of classnames for all listeners within the extension.
*/
public void setListeners(Collection<String> listeners) {
this.listeners = listeners;
}
/** /**
* Returns the path to the small favicon, relative to the root of the * Returns the path to the small favicon, relative to the root of the
* extension. * extension.

View File

@@ -34,6 +34,7 @@ import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.GuacamoleServerException;
import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.AuthenticationProvider;
import org.apache.guacamole.net.event.listener.Listener;
import org.apache.guacamole.resource.Resource; import org.apache.guacamole.resource.Resource;
import org.apache.guacamole.resource.ResourceServlet; import org.apache.guacamole.resource.ResourceServlet;
import org.apache.guacamole.resource.SequenceResource; import org.apache.guacamole.resource.SequenceResource;
@@ -91,6 +92,12 @@ public class ExtensionModule extends ServletModule {
private final List<AuthenticationProvider> boundAuthenticationProviders = private final List<AuthenticationProvider> boundAuthenticationProviders =
new ArrayList<AuthenticationProvider>(); new ArrayList<AuthenticationProvider>();
/**
* All currently-bound authentication providers, if any.
*/
private final List<Listener> boundListeners =
new ArrayList<Listener>();
/** /**
* Service for adding and retrieving language resources. * Service for adding and retrieving language resources.
*/ */
@@ -187,6 +194,49 @@ public class ExtensionModule extends ServletModule {
return Collections.unmodifiableList(boundAuthenticationProviders); return Collections.unmodifiableList(boundAuthenticationProviders);
} }
/**
* Binds the given provider class such that a listener is bound for each
* listener interface implemented by the provider and such that all bound
* listener instances can be obtained via injection.
*
* @param providerClass
* The listener class to bind.
*/
private void bindListener(Class<?> providerClass) {
logger.debug("[{}] Binding listener \"{}\".",
boundListeners.size(), providerClass.getName());
boundListeners.addAll(ListenerFactory.createListeners(providerClass));
}
/**
* Binds each of the the given Listener classes such that any
* service requiring access to the Listener can obtain it via
* injection.
*
* @param listeners
* The Listener classes to bind.
*/
private void bindListeners(Collection<Class<?>> listeners) {
// Bind each listener within extension
for (Class<?> listener : listeners)
bindListener(listener);
}
/**
* Returns a list of all currently-bound Listener instances.
*
* @return
* A List of all currently-bound Listener instances. The List is
* not modifiable.
*/
@Provides
public List<Listener> getListeners() {
return Collections.unmodifiableList(boundListeners);
}
/** /**
* Serves each of the given resources as a language resource. Language * Serves each of the given resources as a language resource. Language
* resources are served from within the "/translations" directory as JSON * resources are served from within the "/translations" directory as JSON
@@ -327,6 +377,9 @@ public class ExtensionModule extends ServletModule {
// Attempt to load all authentication providers // Attempt to load all authentication providers
bindAuthenticationProviders(extension.getAuthenticationProviderClasses()); bindAuthenticationProviders(extension.getAuthenticationProviderClasses());
// Attempt to load all listeners
bindListeners(extension.getListenerClasses());
// Add any translation resources // Add any translation resources
serveLanguageResources(extension.getTranslationResources()); serveLanguageResources(extension.getTranslationResources());

View File

@@ -0,0 +1,278 @@
/*
* 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.extension;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.net.event.AuthenticationFailureEvent;
import org.apache.guacamole.net.event.AuthenticationSuccessEvent;
import org.apache.guacamole.net.event.TunnelCloseEvent;
import org.apache.guacamole.net.event.TunnelConnectEvent;
import org.apache.guacamole.net.event.listener.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A factory that reflectively instantiates Listener objects for a given
* provider class.
*/
class ListenerFactory {
/**
* Creates all listeners represented by an instance of the given provider class.
* <p>
* If a provider class implements the simple Listener interface, that is the
* only listener type that will be returned. Otherwise, a list of Listener
* objects that adapt the legacy listener interfaces will be returned.
*
* @param providerClass
* A class that represents a listener.
*
* @return
* The list of listeners represented by the given provider class.
*/
static List<Listener> createListeners(Class<?> providerClass) {
Object provider = ProviderFactory.newInstance("listener", providerClass);
if (provider instanceof Listener) {
return Collections.singletonList((Listener) provider);
}
return createListenerAdapters(provider);
}
/**
* Creates a list of adapters for the given object, based on the legacy
* listener interfaces it implements.
*
* @param provider
* An object that implements zero or more legacy listener interfaces.
*
* @return
* The list of listeners represented by the given provider class.
*/
@SuppressWarnings("deprecation")
private static List<Listener> createListenerAdapters(Object provider) {
final List<Listener> listeners = new ArrayList<Listener>();
if (provider instanceof AuthenticationSuccessListener) {
listeners.add(new AuthenticationSuccessListenerAdapter(
(AuthenticationSuccessListener) provider));
}
if (provider instanceof AuthenticationFailureListener) {
listeners.add(new AuthenticationFailureListenerAdapter(
(AuthenticationFailureListener) provider));
}
if (provider instanceof TunnelConnectListener) {
listeners.add(new TunnelConnectListenerAdapter(
(TunnelConnectListener) provider));
}
if (provider instanceof TunnelCloseListener) {
listeners.add(new TunnelCloseListenerAdapter(
(TunnelCloseListener) provider));
}
return listeners;
}
/**
* An adapter the allows an AuthenticationSuccessListener to be used
* as an ordinary Listener.
*/
@SuppressWarnings("deprecation")
private static class AuthenticationSuccessListenerAdapter implements Listener {
/**
* The delegate listener for this adapter.
*/
private final AuthenticationSuccessListener delegate;
/**
* Constructs a new adapter that delivers events to the given delegate.
*
* @param delegate
* The delegate listener.
*/
AuthenticationSuccessListenerAdapter(AuthenticationSuccessListener delegate) {
this.delegate = delegate;
}
/**
* Handles an AuthenticationSuccessEvent by passing the event to the delegate
* listener. If the delegate returns false, the adapter throws a GuacamoleException
* to veto the authentication success event. All other event types are ignored.
*
* @param event
* An object that describes the event that occurred.
*
* @throws GuacamoleException
* If thrown by the delegate listener.
*/
@Override
public void handleEvent(Object event) throws GuacamoleException {
if (event instanceof AuthenticationSuccessEvent) {
if (!delegate.authenticationSucceeded((AuthenticationSuccessEvent) event)) {
throw new GuacamoleSecurityException(
"listener vetoed successful authentication");
}
}
}
}
/**
* An adapter the allows an AuthenticationFailureListener to be used
* as an ordinary Listener.
*/
@SuppressWarnings("deprecation")
private static class AuthenticationFailureListenerAdapter implements Listener {
/**
* The delegate listener for this adapter.
*/
private final AuthenticationFailureListener delegate;
/**
* Constructs a new adapter that delivers events to the given delegate.
*
* @param delegate
* The delegate listener.
*/
AuthenticationFailureListenerAdapter(AuthenticationFailureListener delegate) {
this.delegate = delegate;
}
/**
* Handles an AuthenticationFailureEvent by passing the event to the delegate
* listener. All other event types are ignored.
*
* @param event
* An object that describes the event that occurred.
*
* @throws GuacamoleException
* If thrown by the delegate listener.
*/
@Override
public void handleEvent(Object event) throws GuacamoleException {
if (event instanceof AuthenticationFailureEvent) {
delegate.authenticationFailed((AuthenticationFailureEvent) event);
}
}
}
/**
* An adapter the allows a TunnelConnectListener to be used as an ordinary
* Listener.
*/
@SuppressWarnings("deprecation")
private static class TunnelConnectListenerAdapter implements Listener {
/**
* The delegate listener for this adapter.
*/
private final TunnelConnectListener delegate;
/**
* Constructs a new adapter that delivers events to the given delegate.
*
* @param delegate
* The delegate listener.
*/
TunnelConnectListenerAdapter(TunnelConnectListener delegate) {
this.delegate = delegate;
}
/**
* Handles a TunnelConnectEvent by passing the event to the delegate listener.
* If the delegate returns false, the adapter throws a GuacamoleException
* to veto the tunnel connect event. All other event types are ignored.
*
* @param event
* An object that describes the event that occurred.
*
* @throws GuacamoleException
* If thrown by the delegate listener.
*/
@Override
public void handleEvent(Object event) throws GuacamoleException {
if (event instanceof TunnelConnectEvent) {
if (!delegate.tunnelConnected((TunnelConnectEvent) event)) {
throw new GuacamoleException("listener vetoed tunnel connection");
}
}
}
}
/**
* An adapter the allows a TunnelCloseListener to be used as an ordinary
* Listener.
*/
@SuppressWarnings("deprecation")
private static class TunnelCloseListenerAdapter implements Listener {
/**
* The delegate listener for this adapter.
*/
private final TunnelCloseListener delegate;
/**
* Constructs a new adapter that delivers events to the given delegate.
*
* @param delegate
* The delegate listener.
*/
TunnelCloseListenerAdapter(TunnelCloseListener delegate) {
this.delegate = delegate;
}
/**
* Handles a TunnelCloseEvent by passing the event to the delegate listener.
* If the delegate returns false, the adapter throws a GuacamoleException
* to veto the tunnel connect event. All other event types are ignored.
*
* @param event
* An object that describes the event that occurred.
*
* @throws GuacamoleException
* If thrown by the delegate listener.
*/
@Override
public void handleEvent(Object event) throws GuacamoleException {
if (event instanceof TunnelCloseEvent) {
if (!delegate.tunnelClosed((TunnelCloseEvent) event)) {
throw new GuacamoleException("listener vetoed tunnel close request");
}
}
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* 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.extension;
import org.apache.guacamole.GuacamoleException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
/**
* A utility for creating provider instances and logging unexpected outcomes
* with sufficient detail to allow debugging.
*/
class ProviderFactory {
/**
* Logger used to log unexpected outcomes.
*/
private static final Logger logger = LoggerFactory.getLogger(ProviderFactory.class);
/**
* Creates an instance of the specified provider class using the no-arg constructor.
*
* @param typeName
* The provider type name used for log messages; e.g. "authentication provider".
*
* @param providerClass
* The provider class to instantiate.
*
* @param <T>
* The provider type.
*
* @return
* A provider instance or null if no instance was created due to error.
*/
static <T> T newInstance(String typeName, Class<? extends T> providerClass) {
T instance = null;
try {
// Attempt to instantiate the provider
instance = providerClass.getConstructor().newInstance();
}
catch (NoSuchMethodException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.", typeName);
logger.debug("{} is missing a default constructor.",
providerClass.getName(), e);
}
catch (SecurityException e) {
logger.error("The Java security manager is preventing extensions "
+ "from being loaded. Please check the configuration of Java or your "
+ "servlet container.");
logger.debug("Creation of {} disallowed by security manager.",
providerClass.getName(), e);
}
catch (InstantiationException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.", typeName);
logger.debug("{} cannot be instantiated.", providerClass.getName(), e);
}
catch (IllegalAccessException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.");
logger.debug("Default constructor of {} is not public.", typeName, e);
}
catch (IllegalArgumentException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.", typeName);
logger.debug("Default constructor of {} cannot accept zero arguments.",
providerClass.getName(), e);
}
catch (InvocationTargetException e) {
// Obtain causing error - create relatively-informative stub error if cause is unknown
Throwable cause = e.getCause();
if (cause == null)
cause = new GuacamoleException("Error encountered during initialization.");
logger.error("{} extension failed to start: {}", typeName, cause.getMessage());
logger.debug("{} instantiation failed.", providerClass.getName(), e);
}
return instance;
}
}

View File

@@ -19,6 +19,7 @@
package org.apache.guacamole.rest; package org.apache.guacamole.rest;
import org.apache.guacamole.rest.event.ListenerService;
import org.apache.guacamole.rest.session.UserContextResourceFactory; import org.apache.guacamole.rest.session.UserContextResourceFactory;
import org.apache.guacamole.rest.session.SessionRESTService; import org.apache.guacamole.rest.session.SessionRESTService;
import com.google.inject.Scopes; import com.google.inject.Scopes;
@@ -76,6 +77,7 @@ public class RESTServiceModule extends ServletModule {
bind(TokenSessionMap.class).toInstance(tokenSessionMap); bind(TokenSessionMap.class).toInstance(tokenSessionMap);
// Bind low-level services // Bind low-level services
bind(ListenerService.class);
bind(AuthenticationService.class); bind(AuthenticationService.class);
bind(AuthTokenGenerator.class).to(SecureRandomAuthTokenGenerator.class); bind(AuthTokenGenerator.class).to(SecureRandomAuthTokenGenerator.class);

View File

@@ -24,9 +24,11 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.GuacamoleUnauthorizedException; import org.apache.guacamole.GuacamoleUnauthorizedException;
import org.apache.guacamole.GuacamoleSession;
import org.apache.guacamole.environment.Environment; import org.apache.guacamole.environment.Environment;
import org.apache.guacamole.net.auth.AuthenticatedUser; import org.apache.guacamole.net.auth.AuthenticatedUser;
import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.net.auth.AuthenticationProvider;
@@ -35,7 +37,9 @@ import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.auth.credentials.CredentialsInfo; import org.apache.guacamole.net.auth.credentials.CredentialsInfo;
import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleCredentialsException;
import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException; import org.apache.guacamole.net.auth.credentials.GuacamoleInvalidCredentialsException;
import org.apache.guacamole.GuacamoleSession; import org.apache.guacamole.net.event.AuthenticationFailureEvent;
import org.apache.guacamole.net.event.AuthenticationSuccessEvent;
import org.apache.guacamole.rest.event.ListenerService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -74,6 +78,12 @@ public class AuthenticationService {
@Inject @Inject
private AuthTokenGenerator authTokenGenerator; private AuthTokenGenerator authTokenGenerator;
/**
* The service to use to notify registered authentication listeners.
*/
@Inject
private ListenerService listenerService;
/** /**
* Regular expression which matches any IPv4 address. * Regular expression which matches any IPv4 address.
*/ */
@@ -207,6 +217,47 @@ public class AuthenticationService {
} }
/**
* Notify all bound listeners that a successful authentication
* has occurred.
*
* @param authenticatedUser
* The user that was successfully authenticated.
*
* @param session
* The existing session for the user (if any).
*
* @throws GuacamoleException
* If thrown by a listener.
*/
private void fireAuthenticationSuccessEvent(
AuthenticatedUser authenticatedUser, GuacamoleSession session)
throws GuacamoleException {
UserContext userContext = null;
if (session != null) {
userContext = session.getUserContext(
authenticatedUser.getAuthenticationProvider().getIdentifier());
}
listenerService.handleEvent(new AuthenticationSuccessEvent(
userContext, authenticatedUser.getCredentials()));
}
/**
* Notify all bound listeners that an authentication attempt has failed.
*
* @param credentials
* The credentials that failed to authenticate.
*
* @throws GuacamoleException
* If thrown by a listener.
*/
private void fireAuthenticationFailedEvent(Credentials credentials)
throws GuacamoleException {
listenerService.handleEvent(new AuthenticationFailureEvent(credentials));
}
/** /**
* Returns the AuthenticatedUser associated with the given session and * Returns the AuthenticatedUser associated with the given session and
* credentials, performing a fresh authentication and creating a new * credentials, performing a fresh authentication and creating a new
@@ -232,11 +283,17 @@ public class AuthenticationService {
try { try {
// Re-authenticate user if session exists // Re-authenticate user if session exists
if (existingSession != null) if (existingSession != null) {
return updateAuthenticatedUser(existingSession.getAuthenticatedUser(), credentials); AuthenticatedUser updatedUser = updateAuthenticatedUser(
existingSession.getAuthenticatedUser(), credentials);
fireAuthenticationSuccessEvent(updatedUser, existingSession);
return updatedUser;
}
// Otherwise, attempt authentication as a new user // Otherwise, attempt authentication as a new user
AuthenticatedUser authenticatedUser = AuthenticationService.this.authenticateUser(credentials); AuthenticatedUser authenticatedUser = AuthenticationService.this.authenticateUser(credentials);
fireAuthenticationSuccessEvent(authenticatedUser, null);
if (logger.isInfoEnabled()) if (logger.isInfoEnabled())
logger.info("User \"{}\" successfully authenticated from {}.", logger.info("User \"{}\" successfully authenticated from {}.",
authenticatedUser.getIdentifier(), authenticatedUser.getIdentifier(),
@@ -249,6 +306,8 @@ public class AuthenticationService {
// Log and rethrow any authentication errors // Log and rethrow any authentication errors
catch (GuacamoleException e) { catch (GuacamoleException e) {
fireAuthenticationFailedEvent(credentials);
// Get request and username for sake of logging // Get request and username for sake of logging
HttpServletRequest request = credentials.getRequest(); HttpServletRequest request = credentials.getRequest();
String username = credentials.getUsername(); String username = credentials.getUsername();

View File

@@ -0,0 +1,57 @@
/*
* 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.rest.event;
import java.util.List;
import com.google.inject.Inject;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.event.listener.Listener;
/**
* A service used to notify listeners registered by extensions when events of
* interest occur.
*/
public class ListenerService implements Listener {
/**
* The collection of registered listeners.
*/
@Inject
private List<Listener> listeners;
/**
* Notifies registered listeners than an event has occurred. Notification continues
* until a given listener throws a GuacamoleException or other runtime exception, or
* until all listeners have been notified.
*
* @param event
* An object that describes the event that has occurred.
*
* @throws GuacamoleException
* If a registered listener throws a GuacamoleException.
*/
@Override
public void handleEvent(Object event) throws GuacamoleException {
for (final Listener listener : listeners) {
listener.handleEvent(event);
}
}
}

View File

@@ -29,10 +29,14 @@ import org.apache.guacamole.GuacamoleUnauthorizedException;
import org.apache.guacamole.net.GuacamoleTunnel; import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.auth.Connection; import org.apache.guacamole.net.auth.Connection;
import org.apache.guacamole.net.auth.ConnectionGroup; import org.apache.guacamole.net.auth.ConnectionGroup;
import org.apache.guacamole.net.auth.Credentials;
import org.apache.guacamole.net.auth.Directory; import org.apache.guacamole.net.auth.Directory;
import org.apache.guacamole.net.auth.UserContext; import org.apache.guacamole.net.auth.UserContext;
import org.apache.guacamole.net.event.TunnelCloseEvent;
import org.apache.guacamole.net.event.TunnelConnectEvent;
import org.apache.guacamole.rest.auth.AuthenticationService; import org.apache.guacamole.rest.auth.AuthenticationService;
import org.apache.guacamole.protocol.GuacamoleClientInformation; import org.apache.guacamole.protocol.GuacamoleClientInformation;
import org.apache.guacamole.rest.event.ListenerService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -57,6 +61,58 @@ public class TunnelRequestService {
@Inject @Inject
private AuthenticationService authenticationService; private AuthenticationService authenticationService;
/**
* A service for notifying listeners about tunnel connect/closed events.
*/
@Inject
private ListenerService listenerService;
/**
* Notifies bound listeners that a new tunnel has been connected.
* Listeners may veto a connected tunnel by throwing any GuacamoleException.
*
* @param userContext
* The UserContext associated with the user for whom the tunnel is
* being created.
*
* @param credentials
* Credentials that authenticate the user.
*
* @param tunnel
* The tunnel that was connected.
*
* @throws GuacamoleException
* If thrown by a listener or if any listener vetoes the connected tunnel.
*/
private void fireTunnelConnectEvent(UserContext userContext,
Credentials credentials, GuacamoleTunnel tunnel) throws GuacamoleException {
listenerService.handleEvent(new TunnelConnectEvent(userContext, credentials, tunnel));
}
/**
* Notifies bound listeners that a tunnel is to be closed.
* Listeners are allowed to veto a request to close a tunnel by throwing any
* GuacamoleException.
*
* @param userContext
* The UserContext associated with the user for whom the tunnel is
* being created.
*
* @param credentials
* Credentials that authenticate the user.
*
* @param tunnel
* The tunnel that was connected.
*
* @throws GuacamoleException
* If thrown by a listener.
*/
private void fireTunnelClosedEvent(UserContext userContext,
Credentials credentials, GuacamoleTunnel tunnel)
throws GuacamoleException {
listenerService.handleEvent(new TunnelCloseEvent(userContext, credentials, tunnel));
}
/** /**
* Reads and returns the client information provided within the given * Reads and returns the client information provided within the given
* request. * request.
@@ -226,7 +282,7 @@ public class TunnelRequestService {
* @throws GuacamoleException * @throws GuacamoleException
* If an error occurs while obtaining the tunnel. * If an error occurs while obtaining the tunnel.
*/ */
protected GuacamoleTunnel createAssociatedTunnel(GuacamoleTunnel tunnel, protected GuacamoleTunnel createAssociatedTunnel(final GuacamoleTunnel tunnel,
final String authToken, final GuacamoleSession session, final String authToken, final GuacamoleSession session,
final UserContext context, final TunnelRequest.Type type, final UserContext context, final TunnelRequest.Type type,
final String id) throws GuacamoleException { final String id) throws GuacamoleException {
@@ -243,6 +299,10 @@ public class TunnelRequestService {
@Override @Override
public void close() throws GuacamoleException { public void close() throws GuacamoleException {
// notify listeners to allow close request to be vetoed
fireTunnelClosedEvent(context,
session.getAuthenticatedUser().getCredentials(), tunnel);
long connectionEndTime = System.currentTimeMillis(); long connectionEndTime = System.currentTimeMillis();
long duration = connectionEndTime - connectionStartTime; long duration = connectionEndTime - connectionStartTime;
@@ -328,6 +388,10 @@ public class TunnelRequestService {
// Create connected tunnel using provided connection ID and client information // Create connected tunnel using provided connection ID and client information
GuacamoleTunnel tunnel = createConnectedTunnel(userContext, type, id, info); GuacamoleTunnel tunnel = createConnectedTunnel(userContext, type, id, info);
// Notify listeners to allow connection to be vetoed
fireTunnelConnectEvent(userContext,
session.getAuthenticatedUser().getCredentials(), tunnel);
// Associate tunnel with session // Associate tunnel with session
return createAssociatedTunnel(tunnel, authToken, session, userContext, type, id); return createAssociatedTunnel(tunnel, authToken, session, userContext, type, id);