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

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

View File

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

View File

@@ -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().
* <p>
* If a {@link org.apache.guacamole.net.event.listener.Listener} throws
* a GuacamoleException when handling an event of this type, the request to close
* the tunnel is effectively <em>vetoed</em> and will remain connected.
*/
public class TunnelCloseEvent implements UserEvent, CredentialEvent, TunnelEvent {

View File

@@ -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().
* <p>
* If a {@link org.apache.guacamole.net.event.listener.Listener} throws
* a GuacamoleException when handling an event of this type, the tunnel connection
* is effectively <em>vetoed</em> and will be subsequently closed.
*/
public class TunnelConnectEvent implements UserEvent, CredentialEvent, TunnelEvent {

View File

@@ -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;

View File

@@ -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;

View File

@@ -0,0 +1,51 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.net.event.listener;
import org.apache.guacamole.GuacamoleException;
/**
* A listener for events that occur in handing various Guacamole requests
* such as authentication, tunnel connect/close, etc. Listeners are registered
* through the extension manifest mechanism. When an event occurs, listeners
* are notified in the order in which they are declared in the manifest and
* continues until either all listeners have been notified or with the first
* listener that throws a GuacamoleException or other runtime exception.
*/
public interface Listener {
/**
* Notifies the recipient that an event has occurred.
* <p>
* Throwing an exception from an event listener can act to veto an action in
* progress for some event types. See the Javadoc for specific event types for
* details.
*
* @param event
* An object that describes the event that has occurred.
*
* @throws GuacamoleException
* If the listener wishes to stop notification of the event to subsequent
* listeners. For some event types, this acts to veto an action in progress;
* e.g. treating a successful authentication as though it failed.
*/
void handleEvent(Object event) throws GuacamoleException;
}

View File

@@ -23,26 +23,33 @@ import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.event.TunnelCloseEvent;
/**
* 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;
}

View File

@@ -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;
}

View File

@@ -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

View File

@@ -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<Class<AuthenticationProvider>> authenticationProviderClasses;
/**
* The collection of all Listener classes defined within the extension.
*/
private final Collection<Class<?>> listenerClasses;
/**
* The resource for the small favicon for the extension. If provided, this
* will replace the default Guacamole icon.
@@ -265,6 +271,80 @@ public class Extension {
}
/**
* Retrieve the Listener subclass having the given name. If
* the class having the given name does not exist or isn't actually a
* subclass of Listener, an exception will be thrown.
*
* @param name
* The name of the Listener class to retrieve.
*
* @return
* The subclass of Listener having the given name.
*
* @throws GuacamoleException
* If no such class exists, or if the class with the given name is not
* a subclass of Listener.
*/
@SuppressWarnings("unchecked") // We check this ourselves with isAssignableFrom()
private Class<Listener> getListenerClass(String name)
throws GuacamoleException {
try {
// Get listener class
Class<?> listenerClass = classLoader.loadClass(name);
// Verify the located class is actually a subclass of Listener
if (!Listener.class.isAssignableFrom(listenerClass))
throw new GuacamoleServerException("Listeners MUST implement a Listener subclass.");
// Return located class
return (Class<Listener>) listenerClass;
}
catch (ClassNotFoundException e) {
throw new GuacamoleException("Listener class not found.", e);
}
catch (LinkageError e) {
throw new GuacamoleException("Listener class cannot be loaded (wrong version of API?).", e);
}
}
/**
* Returns a new collection of all Listener subclasses having the given names.
* If any class does not exist or isn't actually subclass of Listener, an
* exception will be thrown, an no further Listener classes will be loaded.
*
* @param names
* The names of the AuthenticationProvider classes to retrieve.
*
* @return
* A new collection of all AuthenticationProvider subclasses having the
* given names.
*
* @throws GuacamoleException
* If any given class does not exist, or if any given class is not a
* subclass of AuthenticationProvider.
*/
private Collection<Class<?>> getListenerClasses(Collection<String> names)
throws GuacamoleException {
// If no classnames are provided, just return an empty list
if (names == null)
return Collections.<Class<?>>emptyList();
// Define all auth provider classes
Collection<Class<?>> classes = new ArrayList<Class<?>>(names.size());
for (String name : names)
classes.add(getListenerClass(name));
// Callers should not rely on modifying the result
return Collections.unmodifiableCollection(classes);
}
/**
* Loads the given file as an extension, which must be a .jar containing
* 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<Class<?>> getListenerClasses() {
return listenerClasses;
}
/**
* Returns the resource for the small favicon for the extension. If
* provided, this will replace the default Guacamole icon.

View File

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

View File

@@ -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<AuthenticationProvider> boundAuthenticationProviders =
new ArrayList<AuthenticationProvider>();
/**
* All currently-bound authentication providers, if any.
*/
private final List<Listener> boundListeners =
new ArrayList<Listener>();
/**
* Service for adding and retrieving language resources.
*/
@@ -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<Class<?>> listeners) {
// Bind each listener within extension
for (Class<?> listener : listeners)
bindListener(listener);
}
/**
* Returns a list of all currently-bound Listener instances.
*
* @return
* A List of all currently-bound Listener instances. The List is
* not modifiable.
*/
@Provides
public List<Listener> getListeners() {
return Collections.unmodifiableList(boundListeners);
}
/**
* Serves each of the given resources as a language resource. Language
* 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());

View File

@@ -0,0 +1,278 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.extension;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.GuacamoleSecurityException;
import org.apache.guacamole.net.event.AuthenticationFailureEvent;
import org.apache.guacamole.net.event.AuthenticationSuccessEvent;
import org.apache.guacamole.net.event.TunnelCloseEvent;
import org.apache.guacamole.net.event.TunnelConnectEvent;
import org.apache.guacamole.net.event.listener.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A factory that reflectively instantiates Listener objects for a given
* provider class.
*/
class ListenerFactory {
/**
* Creates all listeners represented by an instance of the given provider class.
* <p>
* If a provider class implements the simple Listener interface, that is the
* only listener type that will be returned. Otherwise, a list of Listener
* objects that adapt the legacy listener interfaces will be returned.
*
* @param providerClass
* A class that represents a listener.
*
* @return
* The list of listeners represented by the given provider class.
*/
static List<Listener> createListeners(Class<?> providerClass) {
Object provider = ProviderFactory.newInstance("listener", providerClass);
if (provider instanceof Listener) {
return Collections.singletonList((Listener) provider);
}
return createListenerAdapters(provider);
}
/**
* Creates a list of adapters for the given object, based on the legacy
* listener interfaces it implements.
*
* @param provider
* An object that implements zero or more legacy listener interfaces.
*
* @return
* The list of listeners represented by the given provider class.
*/
@SuppressWarnings("deprecation")
private static List<Listener> createListenerAdapters(Object provider) {
final List<Listener> listeners = new ArrayList<Listener>();
if (provider instanceof AuthenticationSuccessListener) {
listeners.add(new AuthenticationSuccessListenerAdapter(
(AuthenticationSuccessListener) provider));
}
if (provider instanceof AuthenticationFailureListener) {
listeners.add(new AuthenticationFailureListenerAdapter(
(AuthenticationFailureListener) provider));
}
if (provider instanceof TunnelConnectListener) {
listeners.add(new TunnelConnectListenerAdapter(
(TunnelConnectListener) provider));
}
if (provider instanceof TunnelCloseListener) {
listeners.add(new TunnelCloseListenerAdapter(
(TunnelCloseListener) provider));
}
return listeners;
}
/**
* An adapter the allows an AuthenticationSuccessListener to be used
* as an ordinary Listener.
*/
@SuppressWarnings("deprecation")
private static class AuthenticationSuccessListenerAdapter implements Listener {
/**
* The delegate listener for this adapter.
*/
private final AuthenticationSuccessListener delegate;
/**
* Constructs a new adapter that delivers events to the given delegate.
*
* @param delegate
* The delegate listener.
*/
AuthenticationSuccessListenerAdapter(AuthenticationSuccessListener delegate) {
this.delegate = delegate;
}
/**
* Handles an AuthenticationSuccessEvent by passing the event to the delegate
* listener. If the delegate returns false, the adapter throws a GuacamoleException
* to veto the authentication success event. All other event types are ignored.
*
* @param event
* An object that describes the event that occurred.
*
* @throws GuacamoleException
* If thrown by the delegate listener.
*/
@Override
public void handleEvent(Object event) throws GuacamoleException {
if (event instanceof AuthenticationSuccessEvent) {
if (!delegate.authenticationSucceeded((AuthenticationSuccessEvent) event)) {
throw new GuacamoleSecurityException(
"listener vetoed successful authentication");
}
}
}
}
/**
* An adapter the allows an AuthenticationFailureListener to be used
* as an ordinary Listener.
*/
@SuppressWarnings("deprecation")
private static class AuthenticationFailureListenerAdapter implements Listener {
/**
* The delegate listener for this adapter.
*/
private final AuthenticationFailureListener delegate;
/**
* Constructs a new adapter that delivers events to the given delegate.
*
* @param delegate
* The delegate listener.
*/
AuthenticationFailureListenerAdapter(AuthenticationFailureListener delegate) {
this.delegate = delegate;
}
/**
* Handles an AuthenticationFailureEvent by passing the event to the delegate
* listener. All other event types are ignored.
*
* @param event
* An object that describes the event that occurred.
*
* @throws GuacamoleException
* If thrown by the delegate listener.
*/
@Override
public void handleEvent(Object event) throws GuacamoleException {
if (event instanceof AuthenticationFailureEvent) {
delegate.authenticationFailed((AuthenticationFailureEvent) event);
}
}
}
/**
* An adapter the allows a TunnelConnectListener to be used as an ordinary
* Listener.
*/
@SuppressWarnings("deprecation")
private static class TunnelConnectListenerAdapter implements Listener {
/**
* The delegate listener for this adapter.
*/
private final TunnelConnectListener delegate;
/**
* Constructs a new adapter that delivers events to the given delegate.
*
* @param delegate
* The delegate listener.
*/
TunnelConnectListenerAdapter(TunnelConnectListener delegate) {
this.delegate = delegate;
}
/**
* Handles a TunnelConnectEvent by passing the event to the delegate listener.
* If the delegate returns false, the adapter throws a GuacamoleException
* to veto the tunnel connect event. All other event types are ignored.
*
* @param event
* An object that describes the event that occurred.
*
* @throws GuacamoleException
* If thrown by the delegate listener.
*/
@Override
public void handleEvent(Object event) throws GuacamoleException {
if (event instanceof TunnelConnectEvent) {
if (!delegate.tunnelConnected((TunnelConnectEvent) event)) {
throw new GuacamoleException("listener vetoed tunnel connection");
}
}
}
}
/**
* An adapter the allows a TunnelCloseListener to be used as an ordinary
* Listener.
*/
@SuppressWarnings("deprecation")
private static class TunnelCloseListenerAdapter implements Listener {
/**
* The delegate listener for this adapter.
*/
private final TunnelCloseListener delegate;
/**
* Constructs a new adapter that delivers events to the given delegate.
*
* @param delegate
* The delegate listener.
*/
TunnelCloseListenerAdapter(TunnelCloseListener delegate) {
this.delegate = delegate;
}
/**
* Handles a TunnelCloseEvent by passing the event to the delegate listener.
* If the delegate returns false, the adapter throws a GuacamoleException
* to veto the tunnel connect event. All other event types are ignored.
*
* @param event
* An object that describes the event that occurred.
*
* @throws GuacamoleException
* If thrown by the delegate listener.
*/
@Override
public void handleEvent(Object event) throws GuacamoleException {
if (event instanceof TunnelCloseEvent) {
if (!delegate.tunnelClosed((TunnelCloseEvent) event)) {
throw new GuacamoleException("listener vetoed tunnel close request");
}
}
}
}
}

View File

@@ -0,0 +1,107 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.extension;
import org.apache.guacamole.GuacamoleException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.lang.reflect.InvocationTargetException;
/**
* A utility for creating provider instances and logging unexpected outcomes
* with sufficient detail to allow debugging.
*/
class ProviderFactory {
/**
* Logger used to log unexpected outcomes.
*/
private static final Logger logger = LoggerFactory.getLogger(ProviderFactory.class);
/**
* Creates an instance of the specified provider class using the no-arg constructor.
*
* @param typeName
* The provider type name used for log messages; e.g. "authentication provider".
*
* @param providerClass
* The provider class to instantiate.
*
* @param <T>
* The provider type.
*
* @return
* A provider instance or null if no instance was created due to error.
*/
static <T> T newInstance(String typeName, Class<? extends T> providerClass) {
T instance = null;
try {
// Attempt to instantiate the provider
instance = providerClass.getConstructor().newInstance();
}
catch (NoSuchMethodException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.", typeName);
logger.debug("{} is missing a default constructor.",
providerClass.getName(), e);
}
catch (SecurityException e) {
logger.error("The Java security manager is preventing extensions "
+ "from being loaded. Please check the configuration of Java or your "
+ "servlet container.");
logger.debug("Creation of {} disallowed by security manager.",
providerClass.getName(), e);
}
catch (InstantiationException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.", typeName);
logger.debug("{} cannot be instantiated.", providerClass.getName(), e);
}
catch (IllegalAccessException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.");
logger.debug("Default constructor of {} is not public.", typeName, e);
}
catch (IllegalArgumentException e) {
logger.error("The {} extension in use is not properly defined. "
+ "Please contact the developers of the extension or, if you "
+ "are the developer, turn on debug-level logging.", typeName);
logger.debug("Default constructor of {} cannot accept zero arguments.",
providerClass.getName(), e);
}
catch (InvocationTargetException e) {
// Obtain causing error - create relatively-informative stub error if cause is unknown
Throwable cause = e.getCause();
if (cause == null)
cause = new GuacamoleException("Error encountered during initialization.");
logger.error("{} extension failed to start: {}", typeName, cause.getMessage());
logger.debug("{} instantiation failed.", providerClass.getName(), e);
}
return instance;
}
}

View File

@@ -19,6 +19,7 @@
package org.apache.guacamole.rest;
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);

View File

@@ -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();

View File

@@ -0,0 +1,57 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.guacamole.rest.event;
import java.util.List;
import com.google.inject.Inject;
import org.apache.guacamole.GuacamoleException;
import org.apache.guacamole.net.event.listener.Listener;
/**
* A service used to notify listeners registered by extensions when events of
* interest occur.
*/
public class ListenerService implements Listener {
/**
* The collection of registered listeners.
*/
@Inject
private List<Listener> listeners;
/**
* Notifies registered listeners than an event has occurred. Notification continues
* until a given listener throws a GuacamoleException or other runtime exception, or
* until all listeners have been notified.
*
* @param event
* An object that describes the event that has occurred.
*
* @throws GuacamoleException
* If a registered listener throws a GuacamoleException.
*/
@Override
public void handleEvent(Object event) throws GuacamoleException {
for (final Listener listener : listeners) {
listener.handleEvent(event);
}
}
}

View File

@@ -29,10 +29,14 @@ import org.apache.guacamole.GuacamoleUnauthorizedException;
import org.apache.guacamole.net.GuacamoleTunnel;
import org.apache.guacamole.net.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);