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 extends AuthenticationProvider> 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 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;
+ }
+
+}
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);