diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java index 2e5ae3a17..c72d669fa 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/AuthenticationSuccessEvent.java @@ -26,6 +26,11 @@ import org.apache.guacamole.net.auth.UserContext; * An event which is triggered whenever a user's credentials pass * authentication. The credentials that passed authentication are included * within this event, and can be retrieved using getCredentials(). + *

+ * If a {@link org.apache.guacamole.net.event.listener.Listener} throws + * a GuacamoleException when handling an event of this type, successful authentication + * is effectively vetoed and will be subsequently processed as though the + * authentication failed. */ public class AuthenticationSuccessEvent implements UserEvent, CredentialEvent { diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/TunnelCloseEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/TunnelCloseEvent.java index ab453e87e..c0e2a622e 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/TunnelCloseEvent.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/TunnelCloseEvent.java @@ -28,6 +28,10 @@ import org.apache.guacamole.net.auth.UserContext; * being closed can be accessed through getTunnel(), and the UserContext * associated with the request which is closing the tunnel can be retrieved * with getUserContext(). + *

+ * 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 vetoed and will remain connected. */ public class TunnelCloseEvent implements UserEvent, CredentialEvent, TunnelEvent { diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/TunnelConnectEvent.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/TunnelConnectEvent.java index acf5e8922..62828db8f 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/TunnelConnectEvent.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/TunnelConnectEvent.java @@ -28,6 +28,10 @@ import org.apache.guacamole.net.auth.UserContext; * being connected can be accessed through getTunnel(), and the UserContext * associated with the request which is connecting the tunnel can be retrieved * with getUserContext(). + *

+ * 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 vetoed and will be subsequently closed. */ public class TunnelConnectEvent implements UserEvent, CredentialEvent, TunnelEvent { diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/AuthenticationFailureListener.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/AuthenticationFailureListener.java index 5fcd27b67..6e707e6c5 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/AuthenticationFailureListener.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/AuthenticationFailureListener.java @@ -26,19 +26,26 @@ import org.apache.guacamole.net.event.AuthenticationFailureEvent; * A listener whose authenticationFailed() hook will fire immediately * after a user's authentication attempt fails. Note that this hook cannot * 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 * fails. * - * @param e The AuthenticationFailureEvent describing the authentication - * failure that just occurred. - * @throws GuacamoleException If an error occurs while handling the - * authentication failure event. Note that - * throwing an exception will NOT cause the - * authentication failure to be canceled. + * @param e + * The AuthenticationFailureEvent describing the authentication + * failure that just occurred. + * + * @throws GuacamoleException + * 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) throws GuacamoleException; diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/AuthenticationSuccessListener.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/AuthenticationSuccessListener.java index 7db072c36..6ba05a3f1 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/AuthenticationSuccessListener.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/AuthenticationSuccessListener.java @@ -27,7 +27,11 @@ import org.apache.guacamole.net.event.AuthenticationSuccessEvent; * authentication attempt succeeds. If a user successfully authenticates, * the authenticationSucceeded() hook has the opportunity to cancel the * authentication and force it to fail. + * + * @deprecated + * Listeners should instead implement the {@link Listener} interface. */ +@Deprecated public interface AuthenticationSuccessListener { /** @@ -35,15 +39,18 @@ public interface AuthenticationSuccessListener { * succeeds. The return value of this hook dictates whether the * successful authentication attempt is canceled. * - * @param e The AuthenticationFailureEvent describing the authentication - * failure that just occurred. - * @return true if the successful authentication attempt should be - * allowed, or false if the attempt should be denied, causing - * the attempt to effectively fail. - * @throws GuacamoleException If an error occurs while handling the - * authentication success event. Throwing an - * exception will also cancel the authentication - * success. + * @param e + * The AuthenticationFailureEvent describing the authentication + * failure that just occurred. + * + * @return + * true if the successful authentication attempt should be + * allowed, or false if the attempt should be denied, causing + * the attempt to effectively fail. + * + * @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) throws GuacamoleException; diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/Listener.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/Listener.java new file mode 100644 index 000000000..af480b7e0 --- /dev/null +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/Listener.java @@ -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. + *

+ * 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; + +} diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/TunnelCloseListener.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/TunnelCloseListener.java index 784e4e932..84d765815 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/TunnelCloseListener.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/TunnelCloseListener.java @@ -23,26 +23,33 @@ import org.apache.guacamole.GuacamoleException; 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. + * + * @deprecated + * Listeners should instead implement the {@link Listener} interface. */ +@Deprecated 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 * be closed. * - * @param e The TunnelCloseEvent describing the tunnel being closed and - * any associated credentials. - * @return true if the tunnel should be allowed to be closed, or false - * if the attempt should be denied, causing the attempt to - * 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. + * @param e + * The TunnelCloseEvent describing the tunnel being closed and + * any associated credentials. + * + * @return + * true if the tunnel should be allowed to be closed, or false + * if the attempt should be denied, causing the attempt to + * 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) - throws GuacamoleException; + boolean tunnelClosed(TunnelCloseEvent e) throws GuacamoleException; } diff --git a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/TunnelConnectListener.java b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/TunnelConnectListener.java index da14fe277..e224f7430 100644 --- a/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/TunnelConnectListener.java +++ b/guacamole-ext/src/main/java/org/apache/guacamole/net/event/listener/TunnelConnectListener.java @@ -25,25 +25,32 @@ import org.apache.guacamole.net.event.TunnelConnectEvent; /** * A listener whose tunnelConnected() hook will fire immediately after a new * tunnel is connected. + * + * @deprecated + * Listeners should instead implement the {@link Listener} interface. */ +@Deprecated public interface TunnelConnectListener { /** - * Event hook which fires immediately after a new tunnel is connected. - * The return value of this hook dictates whether the tunnel is made visible - * to the session. - * - * @param e The TunnelConnectEvent describing the tunnel being connected and - * any associated credentials. - * @return true if the tunnel should be allowed to be connected, or false - * if the attempt should be denied, causing the attempt to - * effectively fail. - * @throws GuacamoleException If an error occurs while handling the - * tunnel connect event. Throwing an exception - * will also stop the tunnel from being made - * visible to the session. - */ - boolean tunnelConnected(TunnelConnectEvent e) - throws GuacamoleException; + * Event hook which fires immediately after a new tunnel is connected. + * The return value of this hook dictates whether the tunnel is made visible + * to the session. + * + * @param e + * The TunnelConnectEvent describing the tunnel being connected and + * any associated credentials. + * + * @return + * true if the tunnel should be allowed to be connected, or false + * if the attempt should be denied, causing the attempt to + * effectively fail. + * + * @throws GuacamoleException + * If an error occurs while handling the tunnel connect event. Throwing + * an exception will also stop the tunnel from being made visible to the + * session. + */ + boolean tunnelConnected(TunnelConnectEvent e) throws GuacamoleException; } diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java b/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java index c1e27650c..8dfbe7fee 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/AuthenticationProviderFacade.java @@ -19,7 +19,6 @@ package org.apache.guacamole.extension; -import java.lang.reflect.InvocationTargetException; import java.util.UUID; import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.net.auth.AuthenticatedUser; @@ -66,58 +65,8 @@ public class AuthenticationProviderFacade implements AuthenticationProvider { * The AuthenticationProvider subclass to instantiate. */ public AuthenticationProviderFacade(Class authProviderClass) { - - AuthenticationProvider instance = null; - - 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; - + authProvider = ProviderFactory.newInstance("authentication provider", + authProviderClass); } @Override diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java b/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java index 3183fa21c..dc43b8f00 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java @@ -35,6 +35,7 @@ import java.util.Map; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; +import org.apache.guacamole.net.event.listener.Listener; import org.codehaus.jackson.JsonParseException; import org.codehaus.jackson.map.ObjectMapper; import org.apache.guacamole.GuacamoleException; @@ -109,6 +110,11 @@ public class Extension { */ private final Collection> authenticationProviderClasses; + /** + * The collection of all Listener classes defined within the extension. + */ + private final Collection> listenerClasses; + /** * The resource for the small favicon for the extension. If provided, this * 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 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) 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> getListenerClasses(Collection names) + throws GuacamoleException { + + // If no classnames are provided, just return an empty list + if (names == null) + return Collections.>emptyList(); + + // Define all auth provider classes + Collection> classes = new ArrayList>(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 * a guac-manifest.json file describing its contents. @@ -363,6 +443,9 @@ public class Extension { // Define authentication providers authenticationProviderClasses = getAuthenticationProviderClasses(manifest.getAuthProviders()); + // Define listeners + listenerClasses = getListenerClasses(manifest.getListeners()); + // Get small icon resource if provided if (manifest.getSmallIcon() != null) smallIcon = new ClassPathResource(classLoader, "image/png", manifest.getSmallIcon()); @@ -488,6 +571,17 @@ public class Extension { 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> getListenerClasses() { + return listenerClasses; + } + /** * Returns the resource for the small favicon for the extension. If * provided, this will replace the default Guacamole icon. diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java index 9b9bd9bee..2ed6c7579 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java @@ -87,6 +87,11 @@ public class ExtensionManifest { */ private Collection authProviders; + /** + * The names of all listener classes within this extension, if any. + */ + private Collection listeners; + /** * The path to the small favicon. If provided, this will replace the default * Guacamole icon. @@ -355,6 +360,32 @@ public class ExtensionManifest { 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 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 listeners) { + this.listeners = listeners; + } + /** * Returns the path to the small favicon, relative to the root of the * extension. diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java index 792066c87..a74c4c0d6 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java @@ -34,6 +34,7 @@ import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleServerException; import org.apache.guacamole.environment.Environment; 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.ResourceServlet; import org.apache.guacamole.resource.SequenceResource; @@ -91,6 +92,12 @@ public class ExtensionModule extends ServletModule { private final List boundAuthenticationProviders = new ArrayList(); + /** + * All currently-bound authentication providers, if any. + */ + private final List boundListeners = + new ArrayList(); + /** * Service for adding and retrieving language resources. */ @@ -187,6 +194,49 @@ public class ExtensionModule extends ServletModule { 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> 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 getListeners() { + return Collections.unmodifiableList(boundListeners); + } + /** * Serves each of the given resources as a language resource. Language * 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 bindAuthenticationProviders(extension.getAuthenticationProviderClasses()); + // Attempt to load all listeners + bindListeners(extension.getListenerClasses()); + // Add any translation resources serveLanguageResources(extension.getTranslationResources()); diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ListenerFactory.java b/guacamole/src/main/java/org/apache/guacamole/extension/ListenerFactory.java new file mode 100644 index 000000000..8aa6babb4 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ListenerFactory.java @@ -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. + *

+ * 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 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 createListenerAdapters(Object provider) { + + final List listeners = new ArrayList(); + + 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"); + } + } + } + + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ProviderFactory.java b/guacamole/src/main/java/org/apache/guacamole/extension/ProviderFactory.java new file mode 100644 index 000000000..01fda5719 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ProviderFactory.java @@ -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 + * The provider type. + * + * @return + * A provider instance or null if no instance was created due to error. + */ + static T newInstance(String typeName, Class 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; + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java b/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java index cab4d973b..587d8338e 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/RESTServiceModule.java @@ -19,6 +19,7 @@ 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.SessionRESTService; import com.google.inject.Scopes; @@ -76,6 +77,7 @@ public class RESTServiceModule extends ServletModule { bind(TokenSessionMap.class).toInstance(tokenSessionMap); // Bind low-level services + bind(ListenerService.class); bind(AuthenticationService.class); bind(AuthTokenGenerator.class).to(SecureRandomAuthTokenGenerator.class); diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java index 31abee5f5..b18f00f4a 100644 --- a/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java +++ b/guacamole/src/main/java/org/apache/guacamole/rest/auth/AuthenticationService.java @@ -24,9 +24,11 @@ import java.util.ArrayList; import java.util.List; import java.util.regex.Pattern; import javax.servlet.http.HttpServletRequest; + import org.apache.guacamole.GuacamoleException; import org.apache.guacamole.GuacamoleSecurityException; import org.apache.guacamole.GuacamoleUnauthorizedException; +import org.apache.guacamole.GuacamoleSession; import org.apache.guacamole.environment.Environment; import org.apache.guacamole.net.auth.AuthenticatedUser; 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.GuacamoleCredentialsException; 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.LoggerFactory; @@ -74,6 +78,12 @@ public class AuthenticationService { @Inject private AuthTokenGenerator authTokenGenerator; + /** + * The service to use to notify registered authentication listeners. + */ + @Inject + private ListenerService listenerService; + /** * 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 * credentials, performing a fresh authentication and creating a new @@ -232,11 +283,17 @@ public class AuthenticationService { try { // Re-authenticate user if session exists - if (existingSession != null) - return updateAuthenticatedUser(existingSession.getAuthenticatedUser(), credentials); + if (existingSession != null) { + AuthenticatedUser updatedUser = updateAuthenticatedUser( + existingSession.getAuthenticatedUser(), credentials); + fireAuthenticationSuccessEvent(updatedUser, existingSession); + return updatedUser; + } // Otherwise, attempt authentication as a new user AuthenticatedUser authenticatedUser = AuthenticationService.this.authenticateUser(credentials); + fireAuthenticationSuccessEvent(authenticatedUser, null); + if (logger.isInfoEnabled()) logger.info("User \"{}\" successfully authenticated from {}.", authenticatedUser.getIdentifier(), @@ -249,6 +306,8 @@ public class AuthenticationService { // Log and rethrow any authentication errors catch (GuacamoleException e) { + fireAuthenticationFailedEvent(credentials); + // Get request and username for sake of logging HttpServletRequest request = credentials.getRequest(); String username = credentials.getUsername(); diff --git a/guacamole/src/main/java/org/apache/guacamole/rest/event/ListenerService.java b/guacamole/src/main/java/org/apache/guacamole/rest/event/ListenerService.java new file mode 100644 index 000000000..e92cc8a66 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/rest/event/ListenerService.java @@ -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 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); + } + } + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java index 628386916..b029a3050 100644 --- a/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java +++ b/guacamole/src/main/java/org/apache/guacamole/tunnel/TunnelRequestService.java @@ -29,10 +29,14 @@ import org.apache.guacamole.GuacamoleUnauthorizedException; import org.apache.guacamole.net.GuacamoleTunnel; import org.apache.guacamole.net.auth.Connection; 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.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.protocol.GuacamoleClientInformation; +import org.apache.guacamole.rest.event.ListenerService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -57,6 +61,58 @@ public class TunnelRequestService { @Inject 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 * request. @@ -226,7 +282,7 @@ public class TunnelRequestService { * @throws GuacamoleException * 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 UserContext context, final TunnelRequest.Type type, final String id) throws GuacamoleException { @@ -243,6 +299,10 @@ public class TunnelRequestService { @Override 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 duration = connectionEndTime - connectionStartTime; @@ -328,6 +388,10 @@ public class TunnelRequestService { // Create connected tunnel using provided connection ID and client information 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 return createAssociatedTunnel(tunnel, authToken, session, userContext, type, id);